Compare commits

...

83 Commits

Author SHA1 Message Date
Brad Fitzpatrick
0d3e653c1b cmd/hello: new hello.ipn.dev server
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-29 12:22:56 -08:00
Brad Fitzpatrick
c611d8480b cmd/tailscaled: add whois/identd-ish debug handler 2021-01-28 15:31:52 -08:00
Brad Fitzpatrick
c7fc4a06da wgengine/router: don't configure IPv6 on Linux when IPv6 is unavailable
Fixes #1214

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-28 13:35:11 -08:00
David Anderson
de497358b8 cmd/tailscaled: add /run to the allowed paths for iptables.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-28 12:58:07 -08:00
Josh Bleecher Snyder
1e28207a15 types/logger: fix rateFree interaction with verbosity prefixes
We log lines like this:

c.logf("[v1] magicsock: disco: %v->%v (%v, %v) sent %v", c.discoShort, dstDisco.ShortString(), dstKey.ShortString(), derpStr(dst.String()), disco.MessageSummary(m))

The leading [v1] causes it to get unintentionally rate limited.
Until we have a proper fix, work around it.

Fixes #1216

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-28 10:15:56 -08:00
David Anderson
7a16ac80b7 VERSION.txt: this is 1.5.0. 2021-01-27 18:45:22 -08:00
Brad Fitzpatrick
4d943536f1 wgengine: don't leak TUN device in NewUserspaceEngine error path
Updates #1187

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-27 11:06:56 -08:00
Brad Fitzpatrick
9f5b0d058f wgengine: fix bugs from earlier fix
Fixes a regression from e970ed0995 that wasn't covered by tests
in this repo. (Our end-to-end tests in another repo caught this.)

Updates #1204
2021-01-27 10:32:08 -08:00
Sonia Appasamy
4dab0c1702 tailcfg: update node display name fields and methods (#1207)
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>

Consolidates the node display name logic from each of the clients into
tailcfg.Node. UI clients can use these names directly, rather than computing
them independently.
2021-01-27 11:50:31 -05:00
Brad Fitzpatrick
35e10c78fc net/interfaces: don't send over zt* interfaces
Fixes #1208

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-26 15:20:43 -08:00
David Anderson
692a011b54 net/interfaces: remove IsTailscaleIP, make callers use tsaddr.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-26 15:10:51 -08:00
Brad Fitzpatrick
e970ed0995 wgengine: fix crash reading long UAPI lines from legacy peers
Also don't log.Fatalf in a function returning an error.

Fixes #1204

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-26 11:39:13 -08:00
Brad Fitzpatrick
a7edcd0872 ipn/ipnstate: update tailscale status -web to match CLI 2021-01-26 08:29:59 -08:00
Brad Fitzpatrick
a98538f84a Merge branch 'main' of github.com:tailscale/tailscale into main 2021-01-25 15:53:13 -08:00
Brad Fitzpatrick
c3c59445ff ipn/ipnserver: on Windows in unattended mode, wait for Engine forever
Updates #1187
2021-01-25 15:52:24 -08:00
Brad Fitzpatrick
0dde8fa0a8 ipn/ipnserver: rearrange some code
No functional change. Make a future diff easier to read.
2021-01-25 15:46:39 -08:00
Brad Fitzpatrick
4d3c09ced4 ipn/ipnserver: on Windows in unattended mode, wait for Engine forever
Updates #1187
2021-01-25 15:32:13 -08:00
Sonia Appasamy
567c5a6d9e tailcfg, controlclient: add DisplayName field to tailcfg.Node and populate it from controlclient (#1191)
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2021-01-25 17:41:39 -05:00
Brad Fitzpatrick
4fea604979 wgengine/router: stop setPrivateNetwork goroutine on configureInterface failure
On Windows, configureInterface starts a goroutine reconfiguring the
Windows firewall.

But if configureInterface fails later, that goroutine kept running and
likely failing forever, spamming logs. Make it stop quietly if its
launching goroutine filed.
2021-01-25 13:22:51 -08:00
Andrey Petrov
bf6205d200 LICENSE: Reformat for Github
Should be equivalent to the license before, but compatible with the library Github uses to detect the license for the project's metadata: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository#detecting-a-license

Signed-off-by: Andrey Petrov <andrey.petrov@shazow.net>
2021-01-24 16:20:22 -08:00
David Anderson
9f7cbf6cf1 wgengine/filter: add a Clone method.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-22 17:31:37 -08:00
Brad Fitzpatrick
9ce92aad3e cmd/tailscaled: update depaware.txt 2021-01-22 14:44:40 -08:00
Brad Fitzpatrick
fa3543d629 control/controlclient: use more direct way of getting the MagicDNS suffix
Suggested by Avery earlier. Ends up fixing bug in "tailscale status" when
MagicDNS if off too:
https://forum.tailscale.com/t/1-3-293-is-released-a-1-4-0-pre-release/349/11?u=bradfitz
2021-01-22 14:30:56 -08:00
Brad Fitzpatrick
e7bf144c3f ipn, wgengine/filter: fix Shields Up recent regression and old bug
Fixes #1192 (regression)
Fixes #1193 (old bug)
2021-01-22 13:39:53 -08:00
Brad Fitzpatrick
97496a83af wgengine/tstun: also support DropSilently on PostFilterIn
Not a problem (yet). But should be consistent with other places that support both
types of drops.
2021-01-22 13:22:32 -08:00
Brad Fitzpatrick
eb47cba435 cmd/tailscaled: don't require --state for --cleanup 2021-01-22 11:35:22 -08:00
Brad Fitzpatrick
daf2c70a08 go.mod: bump wireguard-go 2021-01-21 20:03:35 -08:00
Josh Bleecher Snyder
d5baeeed5c wgengine: use Tailscale-style peer identifiers in logs
Rewrite log lines on the fly, based on the set of known peers.

This enables us to use upstream wireguard-go logging,
but maintain the Tailscale-style peer public key identifiers
that the rest of our systems (and people) expect.

Fixes #1183

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-21 19:13:32 -08:00
Brad Fitzpatrick
4306433d1c cmd/tailscale: make "tailscale ping" also resolve names without DNS
This lets "tailscale ping $NAME" work even if MagicDNS is off, letting you
ping a name that shows up in "tailscale status".

More user friendly.
2021-01-21 15:45:36 -08:00
Brad Fitzpatrick
9541886856 wgengine/magicsock: disable regular STUNs for all platforms by default
Reduces background CPU & network.

Updates #1034

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 14:56:07 -08:00
David Anderson
49d00b6a28 tailcfg: add StableID to Node. #1178
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-21 13:33:19 -08:00
Brad Fitzpatrick
54d0d83b67 safesocket: on Linux, make /var/run/tailscale be 0755
Continuation of earlier two umask changes,
5611f290eb and
d6e9fb1df0.

This change mostly affects us, running tailscaled as root by hand (wit
a umask of 0077), not under systemd. End users running tailscaled
under systemd won't have a umask.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 13:23:14 -08:00
Steve Coffman
fec9490378 Add docker build script to inject version information
Signed-off-by: Steve Coffman <steve@khanacademy.org>
2021-01-21 12:42:53 -08:00
Brad Fitzpatrick
c55d26967b wgengine/magicsock: log more details of endpoints learned over disco
Also, don't try to use IPv6 LinkLocalUnicast addresses for now. Like endpoints
exchanged with control, we share them but don't yet use them.

Updates #1172
2021-01-21 08:06:14 -08:00
Brad Fitzpatrick
9f1b02699a tstime: add RandomDurationBetween helper
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 07:54:14 -08:00
Brad Fitzpatrick
a905ce5607 control/controlclient: add debug knob to not use control's endpoints 2021-01-20 21:31:06 -08:00
Brad Fitzpatrick
359055d3fa wgengine/magicsock: fix logging regression
c8c493f3d9 made it always say
`created=false` which scared me when I saw it, as that would've implied
things were broken much worse. Fortunately the logging was just wrong.
2021-01-20 20:48:02 -08:00
Brad Fitzpatrick
b5628cee4e control/controlclient: add detail to verbose log about route skips 2021-01-20 19:28:21 -08:00
Brad Fitzpatrick
edf64e0901 wgengine/magicsock: send, use endpoints in CallMeMaybe messages
Fixes #1172

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 14:59:41 -08:00
Brad Fitzpatrick
ec77b80c53 tailcfg, control/controlclient: add mapver 10: MapResponse.PeerSeenChange
This adds a more wire-efficient way of updating peers' Node.LastSeen times.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 13:20:21 -08:00
Brad Fitzpatrick
b5b4992eff disco: support parsing/encoding endpoints in call-me-maybe frames
Updates #1172

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 12:16:33 -08:00
Josh Bleecher Snyder
d3dd7c6270 wgengine/magicsock: make legacy DstToString match Addrs
DstToString is used in two places in wireguard-go: Logging and uapi.

We are switching to use uapi for wireguard-go config.
To preserve existing behavior, we need the full set of addrs.

And for logging, having the full set of addrs seems useful.

(The Addrs method itself is slated for removal. When that happens,
the implementation will move to DstToString.)


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-20 10:31:51 -08:00
Brad Fitzpatrick
187e22a756 wgengine/magicsock: don't run the DERP cleanup so often
To save CPU and wakeups, don't run the DERP cleanup timer regularly
unless there is a non-home DERP connection open.

Also eliminates the goroutine, moving to a time.AfterFunc.

Updates #1034

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-19 18:14:25 -08:00
David Anderson
ab9cccb292 cmd/tailscale/cli: require v4 and v6 default routes to be advertised together.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-19 16:49:06 -08:00
David Anderson
78338ac029 types/logger: trim spaces from the rate-limited example message.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-19 16:48:44 -08:00
Brad Fitzpatrick
b405644f5d api.md: add TOC 2021-01-19 12:35:09 -08:00
Josh Bleecher Snyder
5fe5402fcd Revert "wgengine/magicsock: shortcircuit discoEndpoint.heartbeat when its connection is closed"
This reverts commit 08baa17d9a.
It caused deadlocks due to lock ordering violations.
It was not the right fix, and thus should simply be reverted
while we look for the right fix (if we haven't already found it
in the interim; we've fixed other logging-after-test issues).

Fixes #1161
2021-01-19 11:44:32 -08:00
Josh Bleecher Snyder
e4c075cd95 wgengine/magicsock: prevent log-after-test in TestTwoDevicePing 2021-01-19 11:04:17 -08:00
Brad Fitzpatrick
edce91a8a6 wgengine/magicsock: fix a naked return bug/crash where we returned (nil, true)
The 'ok' from 'ipp, ok :=' above was the result parameter ok. Whoops.
2021-01-19 10:57:40 -08:00
Brad Fitzpatrick
51bd1feae4 wgengine/magicsock: add single element IPPort->endpoint cache in receive path
name           old time/op  new time/op  delta
ReceiveFrom-4  21.8µs ± 2%  20.9µs ± 2%  -4.27%  (p=0.000 n=10+10)

Updates #414

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-18 21:40:58 -08:00
David Anderson
da4ec54756 tailcfg: remove v6-overlay debug option.
It's about to become a no-op in control.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-18 17:47:23 -08:00
Brad Fitzpatrick
5c619882bc wgengine/magicsock: simplify ReceiveIPv4+DERP path
name           old time/op  new time/op  delta
ReceiveFrom-4  35.8µs ± 3%  21.9µs ± 5%  -38.92%  (p=0.008 n=5+5)

Fixes #1145
Updates #414

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-18 15:23:17 -08:00
David Anderson
9936cffc1a wgengine: correctly track all node IPs in lazy config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-18 13:32:16 -08:00
Brad Fitzpatrick
3fa86a8b23 wgengine/magicsock: use relatively new netaddr.IPPort.IsZero method 2021-01-15 19:21:10 -08:00
Brad Fitzpatrick
4811236189 wgengine/magicsock: speed up BenchmarkReceiveFrom, store context.Done chan
context.cancelCtx.Done involves a mutex and isn't as cheap as I
previously assumed. Convert the donec method into a struct field and
store the channel value once. Our one magicsock.Conn gets one pointer
larger, but it cuts ~1% of the CPU time of the ReceiveFrom benchmark
and removes a bubble from the --svg output :)
2021-01-15 19:19:27 -08:00
Brad Fitzpatrick
c78ed5b399 go.sum: update (forgotten after earlier wireguard-go update again) 2021-01-15 19:19:27 -08:00
Denton Gentry
013da6660e logtail: add tests
+ add a test for parseAndRemoveLogLevel()
+ add a test for drainPendingMessages()
+ test JSON log encoding including several special cases

Other tests frequently send logs but a) don't check the result and
b) do so by happenstance, such that the code in encode() was not
consistently being exercised and leading to spurious changes in
code coverage. These tests attempt to more systematically test
the logging function.

This is the second attempt to add these tests, the first attempt
(in https://github.com/tailscale/tailscale/pull/1114) had two issues:
1. httptest.NewServer creates multiple goroutine handlers, and
   logtail uses goroutines to upload, but the first version had no
   locking in the server to guard this.
   Moved data handling into channels to get synchronization.
2. The channel to notify the test of the arrival of data had a depth
   of 1, in cases where the Logger sent multiple uploads it would
   block the server.

This resulted in the first iteration of these tests being flaky,
and we reverted it.

This new version of the tests has passed with
    go test -race -count=10000
and seems solid.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-01-15 19:11:40 -08:00
Denton Gentry
8578b0445d tstun: add test to send a packet after Close()
This test serves two purposes:
+ check that Write() returns an error if the tstun has been
  closed.
+ ensure that the close-related code in tstun is exercised in
  a test case. We were getting spurious code coverage adds/drops
  based on timing of when the test case finished.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-01-15 19:11:40 -08:00
Josh Bleecher Snyder
7c1a9e8616 net/nettest: de-flake tests on Windows
Windows has a low resolution timer.
Some of the tests assumed that unblock takes effect immediately.

Consider:

t := time.Now()
elapsed := time.Now().After(t)

It seems plausible that elapsed should always be true.
However, with a low resolution timer, that might fail.

Change time.Now().After to !time.Now().Before,
so that unblocking always takes effect immediately.

Fixes #873.
2021-01-15 18:21:56 -08:00
Josh Bleecher Snyder
a64d06f15c net/nettest: remove pointless checks in tests
If err == nil, then !errors.Is(err, anything).
2021-01-15 18:21:56 -08:00
Josh Bleecher Snyder
503db5540f net/nettest: add missing check at end of TestLimit
This appears to have been an oversight.
2021-01-15 18:21:56 -08:00
Josh Bleecher Snyder
ed2169ae99 wgengine/magicsock: prevent logging after TestActiveDiscovery completes 2021-01-15 18:19:20 -08:00
Josh Bleecher Snyder
12bb949178 go.mod: bump to pull in minor wireguard-go changes 2021-01-15 17:35:03 -08:00
Josh Bleecher Snyder
63af950d8c wgengine/magicsock: adapt to wireguard-go without UpdateDst
22507adf54 stopped relying on
our fork of wireguard-go's UpdateDst callback.
As a result, we can unwind that code,
and the extra return value of ReceiveIPv{4,6}.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 17:13:58 -08:00
Denton Gentry
23c2dc2165 magicksock: remove TestConnClosing. (#1140)
Test is flakey, remove it and figure out what to do differently later.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-01-15 16:55:30 -08:00
David Anderson
e23b4191c4 wgengine/magicsock: disable legacy networking everywhere except TwoDevicePing.
TwoDevicePing is explicitly testing the behavior of the legacy codepath, everything
else is happy to assume that code no longer exists.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 16:02:31 -08:00
David Anderson
0733c5d2e0 wgengine/magicsock: disable legacy behavior in a few more tests.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 15:57:41 -08:00
David Anderson
57d95dd005 wgengine/magicsock: default legacy networking to off for some tests.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 15:54:45 -08:00
David Anderson
a2463e8948 wgengine/magicsock: add an option to disable legacy peer handling.
Used in tests to ensure we're not relying on behavior we're going
to remove eventually.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 15:01:33 -08:00
David Anderson
d456bfdc6d wgengine/magicsock: fix BenchmarkReceiveFrom.
Previously, this benchmark relied on behavior of the legacy
receive codepath, which I changed in 22507adf. With this
change, the benchmark instead relies on the new active discovery
path.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 15:01:33 -08:00
Josh Bleecher Snyder
2d837f79dc wgengine/magicsock: close test loggers once we're done with them
This is a big hammer approach to helping with #1132.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 14:44:56 -08:00
Josh Bleecher Snyder
08baa17d9a wgengine/magicsock: shortcircuit discoEndpoint.heartbeat when its connection is closed
This prevents us from continuing to do unnecessary work
(including logging) after the connection has closed.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 14:44:56 -08:00
Josh Bleecher Snyder
7c76435bf7 wgengine/magicsock: simplify
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 14:44:56 -08:00
Josh Bleecher Snyder
d2529affa2 wgengine/magicsock: quiet wireguard-go logging in tests
We already do this in newUserspaceEngineAdvanced.
Apply it to newMagicStack as well.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 14:44:56 -08:00
Josh Bleecher Snyder
3ad7c2133a wgengine/userspace: make wireguard-go log silencing include peer routines
Also suppress log lines like:

peer(Kksd…ySmc) - Routine: sequential sender - stopped

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-15 14:44:56 -08:00
Brad Fitzpatrick
b560386c1a net/packet, wgengine, tstun: add inter-node TSMP protocol for connect errors
This adds a new IP Protocol type, TSMP on protocol number 99 for
sending inter-tailscale messages over WireGuard, currently just for
why a peer rejects TCP SYNs (ACL rejection, shields up, and in the
future: nothing listening, something listening on that port but wrong
interface, etc)

Updates #1094
Updates tailscale/corp#1185

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-15 14:03:57 -08:00
David Anderson
01e8b7fb7e go.mod: bump wireguard-go version.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-15 10:53:49 -08:00
Brad Fitzpatrick
5611f290eb ipn, ipnserver: only require sudo on Linux for mutable CLI actions
This partially reverts d6e9fb1df0, which modified the permissions
on the tailscaled Unix socket and thus required "sudo tailscale" even
for "tailscale status".

Instead, open the permissions back up (on Linux only) but have the
server look at the peer creds and only permit read-only actions unless
you're root.

In the future we'll also have a group that can do mutable actions.

On OpenBSD and FreeBSD, the permissions on the socket remain locked
down to 0600 from d6e9fb1df0.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-15 10:13:00 -08:00
Brad Fitzpatrick
a45665426b cmd/tailscale/cli: tweak the status name column a bit
* make peers without DNS names show their hostnames as always one column, for cut/etc users
* remove trailing dot from shared peers' DNS names
2021-01-15 07:46:58 -08:00
Naman Sood
420c7a35e2 wgengine/netstack: use tailscale IPs instead of a hardcoded one (#1131)
Signed-off-by: Naman Sood <mail@nsood.in>
2021-01-15 09:16:28 -05:00
Brad Fitzpatrick
3ac952d4e9 go.sum: update 2021-01-14 20:19:44 -08:00
Brad Fitzpatrick
a4b39022e0 wgengine/tsdns: fix MagicDNS lookups of shared nodes
Fixes tailscale/corp#1184
2021-01-14 14:49:32 -08:00
Brad Fitzpatrick
b00c0e5f60 go.sum: update 2021-01-14 14:49:32 -08:00
65 changed files with 2715 additions and 828 deletions

View File

@@ -48,6 +48,9 @@ RUN go mod download
COPY . .
ARG goflags_arg # default intentionally unset
ENV GOFLAGS=$goflags_arg
RUN go install -v ./cmd/...
FROM alpine:3.11

46
LICENSE
View File

@@ -1,27 +1,29 @@
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Tailscale Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1 +1 @@
1.3.0
1.5.0

62
api.md
View File

@@ -5,7 +5,28 @@ The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JS
# Authentication
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
# APIS
# APIs
* **[Devices](#device)**
- [GET device](#device-get)
- [DELETE device](#device-delete)
- Routes
- [GET device routes](#device-routes-get)
- [POST device routes](#device-routes-post)
* **[Tailnets](#tailnet)**
- ACLs
- [GET tailnet ACL](#tailnet-acl-get)
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [DNS](#tailnet-dns)
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
## Device
<!-- TODO: description about what devices are -->
@@ -16,6 +37,8 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
Find the device you're looking for and get the "id" field.
This is your deviceID.
<a name=device-get></div>
#### `GET /api/v2/device/:deviceid` - lists the details for a device
Returns the details for the specified device.
Supply the device of interest in the path using its ID.
@@ -103,6 +126,7 @@ Response
}
```
<a name=device-delete></div>
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
Deletes the provided device from its tailnet.
@@ -139,6 +163,8 @@ If the device is not owned by your tailnet:
```
<a name=device-routes-get></div>
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
@@ -166,6 +192,8 @@ Response
}
```
<a name=device-routes-post></div>
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
@@ -210,7 +238,8 @@ A tailnet is the name of your Tailscale network.
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
"alice@example.com" belongs to the "example.com" tailnet and would use the following format for API calls:
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
```
GET /api/v2/tailnet/example.com/...
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
@@ -218,21 +247,21 @@ curl https://api.tailscale.com/api/v2/tailnet/example.com/...
For solo plans, the tailnet is the email you signed up with.
So "alice@gmail.com" has the tailnet "alice@gmail.com" since @gmail.com is a shared email host.
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
Her API calls would have the following format:
```
GET /api/v2/tailnet/alice@gmail.com/...
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
```
Tailnets are a top level resource. ACL is an example of a resource that is tied to a top level tailnet.
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
### ACL
<a name=tailnet-acl-get></a>
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
@@ -334,6 +363,8 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
}
```
<a name=tailnet-acl-post></a>
#### `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.
@@ -405,6 +436,8 @@ Response
}
```
<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.
@@ -449,8 +482,12 @@ Response
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```
<a name=tailnet-devices></a>
### Devices
<a name=tailnet-devices-get></a>
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
Lists the devices in a tailnet.
Supply the tailnet of interest in the path.
@@ -531,9 +568,12 @@ Response
}
```
<a name=tailnet-dns></a>
### DNS
<a name=tailnet-dns-nameservers-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
Lists the DNS nameservers for a tailnet.
Supply the tailnet of interest in the path.
@@ -556,6 +596,8 @@ Response
}
```
<a name=tailnet-dns-nameservers-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
Supply the tailnet of interest in the path.
@@ -608,6 +650,8 @@ Response:
}
```
<a name=tailnet-dns-preferences-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
Retrieves the DNS preferences that are currently set for the given tailnet.
Supply the tailnet of interest in the path.
@@ -629,6 +673,8 @@ Response:
}
```
<a name=tailnet-dns-preferences-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
Note that MagicDNS is dependent on DNS servers.
@@ -673,6 +719,8 @@ If there are DNS servers:
}
```
<a name=tailnet-dns-searchpaths-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
Retrieves the list of search paths that is currently set for the given tailnet.
Supply the tailnet of interest in the path.
@@ -695,6 +743,8 @@ Response:
}
```
<a name=tailnet-dns-searchpaths-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.

31
build_docker.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for docker distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries inside docker, so that we can track down user
# issues.
#
############################################################################
#
# WARNING: Tailscale is not yet officially supported in Docker,
# Kubernetes, etc.
#
# It might work, but we don't regularly test it, and it's not as polished as
# our currently supported platforms. This is provided for people who know
# how Tailscale works and what they're doing.
#
# Our tracking bug for officially support container use cases is:
# https://github.com/tailscale/tailscale/issues/504
#
# Also, see the various bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
set -eu
eval $(./version/version.sh)
GOFLAGS='-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}"
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -t tailscale:tailscale .

116
cmd/hello/hello.go Normal file
View File

@@ -0,0 +1,116 @@
// 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 hello binary runs hello.ipn.dev.
package main // import "tailscale.com/cmd/hello"
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"strings"
"tailscale.com/tailcfg"
)
var (
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
)
func main() {
flag.Parse()
http.HandleFunc("/", root)
log.Printf("Starting hello server.")
errc := make(chan error, 1)
if *httpAddr != "" {
log.Printf("running HTTP server on %s", *httpAddr)
go func() {
errc <- http.ListenAndServe(*httpAddr, nil)
}()
}
if *httpsAddr != "" {
log.Printf("running HTTPS server on %s", *httpsAddr)
go func() {
errc <- http.ListenAndServeTLS(*httpsAddr,
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
nil,
)
}()
}
log.Fatal(<-errc)
}
func slurpHTML() string {
slurp, err := ioutil.ReadFile("hello.tmpl.html")
if err != nil {
log.Fatal(err)
}
return string(slurp)
}
var tmpl = template.Must(template.New("home").Parse(slurpHTML()))
type tmplData struct {
DisplayName string // "Foo Barberson"
LoginName string // "foo@bar.com"
MachineName string // "imac5k"
IP string // "100.2.3.4"
}
func root(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && *httpsAddr != "" {
host := r.Host
if strings.Contains(r.Host, "100.101.102.103") {
host = "hello.ipn.dev"
}
http.Redirect(w, r, "https://"+host, http.StatusFound)
return
}
if r.RequestURI != "/" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "no remote addr", 500)
return
}
who, err := whoIs(ip)
if err != nil {
log.Printf("whois(%q) error: %v", ip, err)
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, tmplData{
DisplayName: who.UserProfile.DisplayName,
LoginName: who.UserProfile.LoginName,
MachineName: who.Node.ComputedName,
IP: ip,
})
}
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
res, err := http.Get("http://127.0.0.1:4242/whois?ip=" + url.QueryEscape(ip))
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
slurp, _ := ioutil.ReadAll(res.Body)
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
}
r := new(tailcfg.WhoIsResponse)
return r, json.NewDecoder(res.Body).Decode(r)
}

17
cmd/hello/hello.tmpl.html Normal file
View File

@@ -0,0 +1,17 @@
<html>
<head>
<title>Hello from Tailscale</title>
</head>
<body>
<h1>Hello!</h1>
<p>
Hello {{.DisplayName}} ({{.LoginName}}) from {{.MachineName}} ({{.IP}}).
</p>
<p>
<b>Your Tailscale is working!</b>
</p>
<p>
Welcome to Tailscale.
</p>
</body>
</html>

View File

@@ -64,34 +64,63 @@ func runPing(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
if len(args) != 1 {
if len(args) != 1 || args[0] == "" {
return errors.New("usage: ping <hostname-or-IP>")
}
hostOrIP := args[0]
var ip string
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
ch := make(chan *ipnstate.PingResult, 1)
prc := make(chan *ipnstate.PingResult, 1)
stc := make(chan *ipnstate.Status, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
ch <- pr
prc <- pr
}
if n.Status != nil {
stc <- n.Status
}
})
go pump(ctx, bc, c)
hostOrIP := args[0]
// If the argument is an IP address, use it directly without any resolution.
if net.ParseIP(hostOrIP) != nil {
ip = hostOrIP
}
// Otherwise, try to resolve it first from the network peer list.
if ip == "" {
bc.RequestStatus()
select {
case st := <-stc:
for _, ps := range st.Peer {
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
ip = ps.TailAddr
break
}
}
case <-ctx.Done():
return ctx.Err()
}
}
// Finally, use DNS.
if ip == "" {
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
n := 0
anyPong := false
for {
@@ -101,7 +130,7 @@ func runPing(ctx context.Context, args []string) error {
select {
case <-timer.C:
fmt.Printf("timeout waiting for ping reply\n")
case pr := <-ch:
case pr := <-prc:
timer.Stop()
if pr.Err != "" {
return errors.New(pr.Err)

View File

@@ -14,7 +14,6 @@ import (
"net"
"net/http"
"os"
"sort"
"strings"
"time"
@@ -181,7 +180,7 @@ func runStatus(ctx context.Context, args []string) error {
}
peers = append(peers, ps)
}
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
ipnstate.SortPeers(peers)
for _, ps := range peers {
active := peerActive(ps)
if statusArgs.active && !active {
@@ -206,19 +205,9 @@ func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
return ps.DNSName[:i]
}
if ps.DNSName != "" {
return ps.DNSName
return strings.TrimRight(ps.DNSName, ".")
}
return fmt.Sprintf("- (%q)", ps.SimpleHostName())
}
func sortKey(ps *ipnstate.PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
}
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {

View File

@@ -120,6 +120,11 @@ func checkIPForwarding() {
}
}
var (
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6default = netaddr.MustParseIPPrefix("::/0")
)
func runUp(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
@@ -139,6 +144,7 @@ func runUp(ctx context.Context, args []string) error {
}
var routes []netaddr.IPPrefix
var default4, default6 bool
if upArgs.advertiseRoutes != "" {
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes {
@@ -149,8 +155,18 @@ func runUp(ctx context.Context, args []string) error {
if ipp != ipp.Masked() {
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
if ipp == ipv4default {
default4 = true
} else if ipp == ipv6default {
default6 = true
}
routes = append(routes, ipp)
}
if default4 && !default6 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
} else if default6 && !default4 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
}
checkIPForwarding()
}

View File

@@ -66,6 +66,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 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 from tailscale.com/wgengine/magicsock
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
@@ -88,6 +89,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
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+

View File

@@ -103,6 +103,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/key from tailscale.com/derp+
@@ -112,7 +113,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
@@ -129,6 +130,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
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+

View File

@@ -103,10 +103,6 @@ func main() {
os.Exit(0)
}
if args.statepath == "" {
log.Fatalf("--state is required")
}
if args.socketpath == "" && runtime.GOOS != "windows" {
log.Fatalf("--socket is required")
}
@@ -140,6 +136,10 @@ func run() error {
return nil
}
if args.statepath == "" {
log.Fatalf("--state is required")
}
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()

View File

@@ -34,6 +34,8 @@ ProtectHome=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadWritePaths=/etc/
ReadWritePaths=/run/
ReadWritePaths=/var/run/
RestrictSUIDSGID=true
SystemCallArchitectures=native

View File

@@ -32,7 +32,9 @@ import (
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
)
var (
@@ -96,7 +98,13 @@ func handleSSH(s ssh.Session) {
s.Exit(1)
return
}
if !interfaces.IsTailscaleIP(ta.IP) {
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
if !ok {
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
s.Exit(1)
return
}
if !tsaddr.IsTailscaleIP(tanetaddr) {
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return

View File

@@ -744,6 +744,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
resp.Peers = filtered
}
if Debug.StripEndpoints {
for _, p := range resp.Peers {
// We need at least one endpoint here for now else
// other code doesn't even create the discoEndpoint.
// TODO(bradfitz): fix that and then just nil this out.
p.Endpoints = []string{"127.9.9.9:456"}
}
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
@@ -762,6 +770,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
c.mu.Unlock()
nm := &NetworkMap{
SelfNode: resp.Node,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
@@ -790,7 +799,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
}
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)
@@ -972,19 +984,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
}
func initDebug() debug {
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
}
@@ -1065,6 +1079,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
}
}
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

View File

@@ -18,13 +18,13 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/wgengine/filter"
)
type NetworkMap struct {
// Core networking
SelfNode *tailcfg.Node
NodeKey tailcfg.NodeKey
PrivateKey wgkey.Private
Expiry time.Time
@@ -63,27 +63,16 @@ type NetworkMap struct {
// TODO(crawshaw): Capabilities []tailcfg.Capability
}
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
// If non-empty, it will neither start nor end with a period.
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
// MagicDNS isn't necessarily in use).
//
// It will neither start nor end with a period.
func (nm *NetworkMap) MagicDNSSuffix() string {
searchPathUsedAsDNSSuffix := func(suffix string) bool {
if dnsname.HasSuffix(nm.Name, suffix) {
return true
}
for _, p := range nm.Peers {
if dnsname.HasSuffix(p.Name, suffix) {
return true
}
}
return false
name := strings.Trim(nm.Name, ".")
if i := strings.Index(name, "."); i != -1 {
name = name[i+1:]
}
for _, d := range nm.DNS.Domains {
if searchPathUsedAsDNSSuffix(d) {
return strings.Trim(d, ".")
}
}
return ""
return name
}
func (nm *NetworkMap) String() string {
@@ -312,12 +301,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits == 0 {
if (flags & AllowDefaultRoute) == 0 {
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
logf("[v1] wgcfg: not accepting default route from %q (%v)",
nodeDebugName(peer), peer.Key.ShortString())
continue
}
} else if cidrIsSubnet(peer, allowedIP) {
if (flags & AllowSubnetRoutes) == 0 {
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",
allowedIP, nodeDebugName(peer), peer.Key.ShortString())
continue
}
}
@@ -328,6 +319,20 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
return cfg, nil
}
func nodeDebugName(n *tailcfg.Node) string {
name := n.Name
if name == "" {
name = n.Hostinfo.Hostname
}
if i := strings.Index(name, "."); i != -1 {
name = name[:i]
}
if name == "" && len(n.Addresses) != 0 {
return n.Addresses[0].String()
}
return name
}
// cidrIsSubnet reports whether cidr is a non-default-route subnet
// exported by node that is not one of its own self addresses.
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {

View File

@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
case TypePong:
return parsePong(ver, p)
case TypeCallMeMaybe:
return CallMeMaybe{}, nil
return parseCallMeMaybe(ver, p)
default:
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
}
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
//
// The recipient may choose to not open a path back, if it's already
// happy with its path. But usually it will.
type CallMeMaybe struct{}
type CallMeMaybe struct {
// MyNumber is what the peer believes its endpoints are.
//
// Prior to Tailscale 1.4, the endpoints were exchanged purely
// between nodes and the control server.
//
// Starting with Tailscale 1.4, clients advertise their endpoints.
// Older clients won't use this, but newer clients should
// use any endpoints in here that aren't included from control.
//
// Control might have sent stale endpoints if the client was idle
// before contacting us. In that case, the client likely did a STUN
// request immediately before sending the CallMeMaybe to recreate
// their NAT port mapping, and that new good endpoint is included
// in this field, but might not yet be in control's endpoints.
// (And in the future, control will stop distributing endpoints
// when clients are suitably new.)
MyNumber []netaddr.IPPort
}
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
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()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port)
p = p[epLength:]
}
return ret
}
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
m = new(CallMeMaybe)
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
return m, nil
}
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
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]),
})
p = p[epLength:]
}
return m, nil
}
// Pong is a response a Ping.
//
// It includes the sender's source IP + port, so it's effectively a
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case CallMeMaybe:
case *CallMeMaybe:
return "call-me-maybe"
default:
return fmt.Sprintf("%#v", m)

View File

@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
},
{
name: "call_me_maybe",
m: CallMeMaybe{},
m: &CallMeMaybe{},
want: "03 00",
},
{
name: "call_me_maybe_endpoints",
m: &CallMeMaybe{
MyNumber: []netaddr.IPPort{
netaddr.MustParseIPPort("1.2.3.4:567"),
netaddr.MustParseIPPort("[2001::3456]:789"),
},
},
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

2
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174

8
go.sum
View File

@@ -288,6 +288,14 @@ github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBW
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY=
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=

View File

@@ -0,0 +1,49 @@
// 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.
// +build linux
package ipnserver
import (
"net"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) {
ro = true // conservative default for naked returns below
uc, ok := c.(*net.UnixConn)
if !ok {
logf("unexpected connection type %T", c)
return
}
raw, err := uc.SyscallConn()
if err != nil {
logf("SyscallConn: %v", err)
return
}
var cred *unix.Ucred
cerr := raw.Control(func(fd uintptr) {
cred, err = unix.GetsockoptUcred(int(fd),
unix.SOL_SOCKET,
unix.SO_PEERCRED)
})
if cerr != nil {
logf("raw.Control: %v", err)
return
}
if err != nil {
logf("raw.Control: %v", err)
return
}
if cred.Uid == 0 {
// root is not read-only.
return false
}
logf("non-root connection from %v (read-only)", cred.Uid)
return true
}

View File

@@ -0,0 +1,27 @@
// 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.
// +build !linux
package ipnserver
import (
"net"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
// And on Darwin, we're not using it yet, as the Darwin
// tailscaled port isn't yet done, and unix.Ucred and
// unix.GetsockoptUcred aren't in x/sys/unix.
// TODO(bradfitz): OpenBSD and FreeBSD should implement this too.
// But their x/sys/unix package is different than Linux, so
// I didn't include it for now.
return false
}

View File

@@ -7,6 +7,7 @@ package ipnserver
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -32,6 +33,7 @@ import (
"tailscale.com/net/netstat"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/pidowner"
"tailscale.com/util/systemd"
@@ -268,6 +270,10 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
defer s.removeAndCloseConn(c)
logf("[v1] incoming control connection")
if isReadonlyConn(c, logf) {
ctx = ipn.ReadonlyContextOf(ctx)
}
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(br)
if err != nil {
@@ -279,7 +285,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
return
}
s.bsMu.Lock()
if err := s.bs.GotCommandMsg(msg); err != nil {
if err := s.bs.GotCommandMsg(ctx, msg); err != nil {
logf("GotCommandMsg: %v", err)
}
gotQuit := s.bs.GotQuit
@@ -355,7 +361,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
if doReset {
s.logf("identity changed; resetting server")
s.bsMu.Lock()
s.bs.Reset()
s.bs.Reset(context.TODO())
s.bsMu.Unlock()
}
}()
@@ -407,7 +413,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
} else {
s.logf("client disconnected; stopping server")
s.bsMu.Lock()
s.bs.Reset()
s.bs.Reset(context.TODO())
s.bsMu.Unlock()
}
}
@@ -499,41 +505,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}()
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
var store ipn.StateStore
if opts.StatePath != "" {
store, err = ipn.NewFileStore(opts.StatePath)
@@ -562,6 +533,82 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
store = &ipn.MemoryStore{}
}
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("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 {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
<-bootRaceWaitForEngine.Done()
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
@@ -575,13 +622,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveHTMLStatus(w, b)
})
opts.DebugMux.Handle("/whois", whoIsHandler{b})
}
server.b = b
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
if opts.AutostartStateKey != "" {
server.bs.GotCommand(&ipn.Command{
server.bs.GotCommand(context.TODO(), &ipn.Command{
Version: version.Long,
Start: &ipn.StartArgs{
Opts: ipn.Options{
@@ -752,6 +800,27 @@ func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
return func() (wgengine.Engine, error) { return eng, nil }
}
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
// not call getEngine concurrently and stops calling getEngine once
// it's returned a working engine.
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
var mu sync.Mutex
var engGood wgengine.Engine
return func() (wgengine.Engine, error) {
mu.Lock()
defer mu.Unlock()
if engGood != nil {
return engGood, nil
}
e, err := getEngine()
if err != nil {
return nil, err
}
engGood = e
return e, nil
}
}
type dummyAddr string
type oneConnListener struct {
conn net.Conn
@@ -817,3 +886,40 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
}
return 0
}
// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler.
type whoIsHandler struct {
b *ipn.LocalBackend
}
func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b := h.b
var ip netaddr.IP
if v := r.FormValue("ip"); v != "" {
var err error
ip, err = netaddr.ParseIP(r.FormValue("ip"))
if err != nil {
http.Error(w, "invalid 'ip' parameter", 400)
return
}
} else {
http.Error(w, "missing 'ip' parameter", 400)
return
}
n, u, ok := b.WhoIs(ip)
if !ok {
http.Error(w, "no match for IP", 404)
return
}
res := &tailcfg.WhoIsResponse{
Node: n,
UserProfile: &u,
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}

View File

@@ -21,14 +21,21 @@ import (
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
)
// Status represents the entire state of the IPN network.
type Status struct {
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
// in the network such as "userfoo.tailscale.net".
// There are no surrounding dots.
// MagicDNSSuffix should be populated regardless of whether a domain
// has MagicDNS enabled.
MagicDNSSuffix string
Peer map[key.Public]*PeerStatus
User map[tailcfg.UserID]tailcfg.UserProfile
@@ -274,13 +281,22 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
f("<table>\n<thead>\n")
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
f("</thead>\n<tbody>\n")
now := time.Now()
var peers []*PeerStatus
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
peers = append(peers, ps)
}
SortPeers(peers)
for _, ps := range peers {
var actAgo string
if !ps.LastWrite.IsZero() {
ago := now.Sub(ps.LastWrite)
@@ -296,40 +312,44 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
owner = owner[:i]
}
}
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
peer.ShortString(),
html.EscapeString(ps.SimpleHostName()),
hostName := ps.SimpleHostName()
dnsName := strings.TrimRight(ps.DNSName, ".")
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, st.MagicDNSSuffix) {
dnsName = dnsName[:i]
}
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
hostName = ""
}
var hostNameHTML string
if hostName != "" {
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
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,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
actAgo,
)
f("<td class=\"aright\">")
f("<td>")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
relay := ps.Relay
if relay != "" {
if active && ps.CurAddr == "" {
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
} else {
f("derp-%v<br>", html.EscapeString(relay))
if active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
}
}
match := false
for _, addr := range ps.Addrs {
if addr == ps.CurAddr {
match = true
f("🔗 <b>%s</b><br>", addr)
} else {
f("%s<br>", addr)
}
}
if ps.CurAddr != "" && !match {
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
}
f("</td>") // end Addrs
f("</tr>\n")
@@ -375,3 +395,17 @@ type PingResult struct {
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
func SortPeers(peers []*PeerStatus) {
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
}
func sortKey(ps *PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
}

View File

@@ -90,6 +90,7 @@ type LocalBackend struct {
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *controlclient.NetworkMap
nodeByAddr map[netaddr.IP]*tailcfg.Node
activeLogin string // last logged LoginName from netMap
engineStatus EngineStatus
endpoints []string
@@ -234,7 +235,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
})
}
}
}
// WhoIs reports the node and user who owns the node with the given IP.
// If ok == true, n and u are valid.
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ip]
if !ok {
return nil, u, false
}
u, ok = b.netMap.UserProfiles[n.User]
if !ok {
return nil, u, false
}
return n, u, true
}
// SetDecompressor sets a decompression function, which must be a zstd
@@ -562,13 +578,13 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
oldFilter := b.e.GetFilter()
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
var prevFilter *filter.Filter // don't reuse old filter state
b.e.SetFilter(filter.New(nil, localNets, prevFilter, b.logf))
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, oldFilter, b.logf))
} else {
b.logf("netmap packet filter: %v", packetFilter)
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
b.e.SetFilter(filter.New(packetFilter, localNets, oldFilter, b.logf))
}
}
@@ -1507,6 +1523,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
b.logf("active login: %v", login)
b.activeLogin = login
}
if nm == nil {
b.nodeByAddr = nil
return
}
// Update the nodeByAddr index.
if b.nodeByAddr == nil {
b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
}
// First pass, mark everything unwanted.
for k := range b.nodeByAddr {
b.nodeByAddr[k] = nil
}
addNode := func(n *tailcfg.Node) {
for _, ipp := range n.Addresses {
if ipp.IsSingleIP() {
b.nodeByAddr[ipp.IP] = n
}
}
}
if nm.SelfNode != nil {
addNode(nm.SelfNode)
}
for _, p := range nm.Peers {
addNode(p)
}
// Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr {
if v == nil {
delete(b.nodeByAddr, k)
}
}
}
// TestOnlyPublicKeys returns the current machine and node public

View File

@@ -6,6 +6,7 @@ package ipn
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
@@ -20,6 +21,24 @@ import (
"tailscale.com/version"
)
type readOnlyContextKey struct{}
// IsReadonlyContext reports whether ctx is a read-only context, as currently used
// by Unix non-root users running the "tailscale" CLI command. They can run "status",
// but not much else.
func IsReadonlyContext(ctx context.Context) bool {
return ctx.Value(readOnlyContextKey{}) != nil
}
// ReadonlyContextOf returns ctx wrapped with a context value that
// will make IsReadonlyContext reports true.
func ReadonlyContextOf(ctx context.Context) context.Context {
if IsReadonlyContext(ctx) {
return ctx
}
return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{})
}
var jsonEscapedZero = []byte(`\u0000`)
type NoArgs struct{}
@@ -111,7 +130,7 @@ func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
// GotCommandMsg parses the incoming message b as a JSON Command and
// calls GotCommand with it.
func (bs *BackendServer) GotCommandMsg(b []byte) error {
func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
cmd := &Command{}
if len(b) == 0 {
return nil
@@ -119,15 +138,15 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
if err := json.Unmarshal(b, cmd); err != nil {
return err
}
return bs.GotCommand(cmd)
return bs.GotCommand(ctx, cmd)
}
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
cmd.Version = version.Long
return bs.GotCommand(cmd)
return bs.GotCommand(ctx, cmd)
}
func (bs *BackendServer) GotCommand(cmd *Command) error {
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
cmd.Version, version.Long)
@@ -141,12 +160,33 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
})
return nil
}
// TODO(bradfitz): finish plumbing context down to all the methods below;
// currently we just check for read-only contexts in this method and
// then never use contexts again.
// Actions permitted with a read-only context:
if c := cmd.RequestEngineStatus; c != nil {
bs.b.RequestEngineStatus()
return nil
} else if c := cmd.RequestStatus; c != nil {
bs.b.RequestStatus()
return nil
} else if c := cmd.Ping; c != nil {
bs.b.Ping(c.IP)
return nil
}
if IsReadonlyContext(ctx) {
msg := "permission denied"
bs.send(Notify{ErrMessage: &msg})
return nil
}
if cmd.Quit != nil {
bs.GotQuit = true
return errors.New("Quit command received")
}
if c := cmd.Start; c != nil {
} else if c := cmd.Start; c != nil {
opts := c.Opts
opts.Notify = bs.send
return bs.b.Start(opts)
@@ -165,27 +205,17 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
} else if c := cmd.SetWantRunning; c != nil {
bs.b.SetWantRunning(*c)
return nil
} else if c := cmd.RequestEngineStatus; c != nil {
bs.b.RequestEngineStatus()
return nil
} else if c := cmd.RequestStatus; c != nil {
bs.b.RequestStatus()
return nil
} else if c := cmd.FakeExpireAfter; c != nil {
bs.b.FakeExpireAfter(c.Duration)
return nil
} else if c := cmd.Ping; c != nil {
bs.b.Ping(c.IP)
return nil
} else {
return fmt.Errorf("BackendServer.Do: no command specified")
}
return fmt.Errorf("BackendServer.Do: no command specified")
}
func (bs *BackendServer) Reset() error {
func (bs *BackendServer) Reset(ctx context.Context) error {
// Tell the backend we got a Logout command, which will cause it
// to forget all its authentication information.
return bs.GotFakeCommand(&Command{Logout: &NoArgs{}})
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
}
type BackendClient struct {

View File

@@ -6,6 +6,7 @@ package ipn
import (
"bytes"
"context"
"testing"
"time"
@@ -81,7 +82,7 @@ func TestClientServer(t *testing.T) {
serverToClientCh <- append([]byte{}, b...)
}
clientToServer := func(b []byte) {
bs.GotCommandMsg(b)
bs.GotCommandMsg(context.TODO(), b)
}
slogf := func(fmt string, args ...interface{}) {
t.Logf("s: "+fmt, args...)

View File

@@ -6,8 +6,12 @@ package logtail
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
@@ -23,28 +27,201 @@ func TestFastShutdown(t *testing.T) {
l := NewLogger(Config{
BaseURL: testServ.URL,
}, t.Logf)
l.Shutdown(ctx)
err := l.Shutdown(ctx)
if err != nil {
t.Error(err)
}
}
func TestUploadMessages(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
uploads := 0
testServ := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uploads += 1
}))
defer testServ.Close()
// maximum number of times a test will call l.Write()
const logLines = 3
l := NewLogger(Config{BaseURL: testServ.URL}, t.Logf)
for i := 1; i < 10; i++ {
type LogtailTestServer struct {
srv *httptest.Server // Log server
uploaded chan []byte
}
func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
ts := LogtailTestServer{}
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
ts.uploaded = make(chan []byte, 2+logLines)
ts.srv = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error("failed to read HTTP request")
}
ts.uploaded <- body
}))
t.Cleanup(ts.srv.Close)
l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
// There is always an initial "logtail started" message
body := <-ts.uploaded
if !strings.Contains(string(body), "started") {
t.Errorf("unknown start logging statement: %q", string(body))
}
return &ts, l
}
func TestDrainPendingMessages(t *testing.T) {
ts, l := NewLogtailTestHarness(t)
for i := 0; i < logLines; i++ {
l.Write([]byte("log line"))
}
l.Shutdown(ctx)
cancel()
if uploads == 0 {
t.Error("no log uploads")
// all of the "log line" messages usually arrive at once, but poll if needed.
body := ""
for i := 0; i <= logLines; i++ {
body += string(<-ts.uploaded)
count := strings.Count(body, "log line")
if count == logLines {
break
}
// if we never find count == logLines, the test will eventually time out.
}
err := l.Shutdown(context.Background())
if err != nil {
t.Error(err)
}
}
func TestEncodeAndUploadMessages(t *testing.T) {
ts, l := NewLogtailTestHarness(t)
tests := []struct {
name string
log string
want string
}{
{
"plain text",
"log line",
"log line",
},
{
"simple JSON",
`{"text": "log line"}`,
"log line",
},
}
for _, tt := range tests {
io.WriteString(l, tt.log)
body := <-ts.uploaded
data := make(map[string]interface{})
err := json.Unmarshal(body, &data)
if err != nil {
t.Error(err)
}
got := data["text"]
if got != tt.want {
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
}
ltail, ok := data["logtail"]
if ok {
logtailmap := ltail.(map[string]interface{})
_, ok = logtailmap["client_time"]
if !ok {
t.Errorf("%s: no client_time present", tt.name)
}
} else {
t.Errorf("%s: no logtail map present", tt.name)
}
}
err := l.Shutdown(context.Background())
if err != nil {
t.Error(err)
}
}
func TestEncodeSpecialCases(t *testing.T) {
ts, l := NewLogtailTestHarness(t)
// -------------------------------------------------------------------------
// JSON log message already contains a logtail field.
io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
body := <-ts.uploaded
data := make(map[string]interface{})
err := json.Unmarshal(body, &data)
if err != nil {
t.Error(err)
}
errorHasLogtail, ok := data["error_has_logtail"]
if ok {
if errorHasLogtail != "LOGTAIL" {
t.Errorf("error_has_logtail: got:%q; want:%q",
errorHasLogtail, "LOGTAIL")
}
} else {
t.Errorf("no error_has_logtail field: %v", data)
}
// -------------------------------------------------------------------------
// special characters
io.WriteString(l, "\b\f\n\r\t"+`"\`)
bodytext := string(<-ts.uploaded)
// json.Unmarshal would unescape the characters, we have to look at the encoded text
escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
if !escaped {
t.Errorf("special characters got %s", bodytext)
}
// -------------------------------------------------------------------------
// skipClientTime to omit the logtail metadata
l.skipClientTime = true
io.WriteString(l, "text")
body = <-ts.uploaded
data = make(map[string]interface{})
err = json.Unmarshal(body, &data)
if err != nil {
t.Error(err)
}
_, ok = data["logtail"]
if ok {
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
}
// -------------------------------------------------------------------------
// lowMem + long string
l.skipClientTime = false
l.lowMem = true
longStr := strings.Repeat("0", 512)
io.WriteString(l, longStr)
body = <-ts.uploaded
data = make(map[string]interface{})
err = json.Unmarshal(body, &data)
if err != nil {
t.Error(err)
}
text, ok := data["text"]
if !ok {
t.Errorf("lowMem: no text %v", data)
}
if n := len(text.(string)); n > 300 {
t.Errorf("lowMem: got %d chars; want <300 chars", n)
}
// -------------------------------------------------------------------------
err = l.Shutdown(context.Background())
if err != nil {
t.Error(err)
}
}
@@ -75,3 +252,54 @@ func TestLoggerWriteLength(t *testing.T) {
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
}
}
func TestParseAndRemoveLogLevel(t *testing.T) {
tests := []struct {
log string
wantLevel int
wantLog string
}{
{
"no level",
0,
"no level",
},
{
"[v1] level 1",
1,
"level 1",
},
{
"level 1 [v1] ",
1,
"level 1 ",
},
{
"[v2] level 2",
2,
"level 2",
},
{
"level [v2] 2",
2,
"level 2",
},
{
"[v3] no level 3",
0,
"[v3] no level 3",
},
}
for _, tt := range tests {
gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
if gotLevel != tt.wantLevel {
t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
tt.log, gotLevel, tt.wantLevel)
}
if string(gotLog) != tt.wantLog {
t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
tt.log, gotLog, tt.wantLog)
}
}
}

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"reflect"
"runtime"
"sort"
"strings"
@@ -39,8 +40,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
continue
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
return ipnet.IP, &iface, nil
if ipnet, ok := a.(*net.IPNet); ok {
nip, ok := netaddr.FromStdIP(ipnet.IP)
if ok && tsaddr.IsTailscaleIP(nip) {
return ipnet.IP, &iface, nil
}
}
}
}
@@ -57,16 +61,21 @@ func maybeTailscaleInterfaceName(s string) bool {
strings.HasPrefix(s, "utun")
}
// IsTailscaleIP reports whether ip is an IP in a range used by
// Tailscale virtual network interfaces.
func IsTailscaleIP(ip net.IP) bool {
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
return tsaddr.IsTailscaleIP(nip)
}
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
func isProblematicInterface(nif *net.Interface) bool {
name := nif.Name
// Don't try to send disco/etc packets over zerotier; they effectively
// DoS each other by doing traffic amplification, both of them
// preferring/trying to use each other for transport. See:
// https://github.com/tailscale/tailscale/issues/1208
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
return true
}
return false
}
// LocalAddresses returns the machine's IP addresses, separated by
// whether they're loopback addresses.
func LocalAddresses() (regular, loopback []string, err error) {
@@ -77,8 +86,10 @@ func LocalAddresses() (regular, loopback []string, err error) {
}
for i := range ifaces {
iface := &ifaces[i]
if !isUp(iface) {
// Down interfaces don't count
if !isUp(iface) || isProblematicInterface(iface) {
// Skip down interfaces and ones that are
// problematic that we don't want to try to
// send Tailscale traffic over.
continue
}
ifcIsLoopback := isLoopback(iface)

View File

@@ -5,30 +5,9 @@
package interfaces
import (
"net"
"testing"
)
func TestIsTailscaleIP(t *testing.T) {
tests := []struct {
ip string
want bool
}{
{"100.81.251.94", true},
{"8.8.8.8", false},
}
for _, tt := range tests {
ip := net.ParseIP(tt.ip)
if ip == nil {
t.Fatalf("failed to parse IP %q", tt.ip)
}
got := IsTailscaleIP(ip)
if got != tt.want {
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
}
}
}
func TestGetState(t *testing.T) {
st, err := GetState()
if err != nil {

View File

@@ -60,7 +60,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
for {
p.mu.Lock()
closed := p.closed
timedout := !p.readTimeout.IsZero() && time.Now().After(p.readTimeout)
timedout := !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout)
blocked := p.blocked
if !closed && !timedout && len(p.buf) > 0 {
n2 := copy(b, p.buf)
@@ -99,7 +99,7 @@ func (p *Pipe) Write(b []byte) (n int, err error) {
for {
p.mu.Lock()
closed := p.closed
timedout := !p.writeTimeout.IsZero() && time.Now().After(p.writeTimeout)
timedout := !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout)
blocked := p.blocked
if !closed && !timedout {
n2 := len(b)

View File

@@ -35,7 +35,7 @@ func TestPipeTimeout(t *testing.T) {
p := NewPipe("p1", 1<<16)
p.SetWriteDeadline(time.Now().Add(-1 * time.Second))
n, err := p.Write([]byte{'h'})
if err == nil || !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
if !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
t.Errorf("missing write timeout got err: %v", err)
}
if n != 0 {
@@ -49,7 +49,7 @@ func TestPipeTimeout(t *testing.T) {
p.SetReadDeadline(time.Now().Add(-1 * time.Second))
b := make([]byte, 1)
n, err := p.Read(b)
if err == nil || !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
if !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
t.Errorf("missing read timeout got err: %v", err)
}
if n != 0 {
@@ -65,7 +65,7 @@ func TestPipeTimeout(t *testing.T) {
if err := p.Block(); err != nil {
t.Fatal(err)
}
if _, err := p.Write([]byte{'h'}); err == nil || !errors.Is(err, ErrWriteTimeout) {
if _, err := p.Write([]byte{'h'}); !errors.Is(err, ErrWriteTimeout) {
t.Fatalf("want write timeout got: %v", err)
}
})
@@ -80,11 +80,10 @@ func TestPipeTimeout(t *testing.T) {
if err := p.Block(); err != nil {
t.Fatal(err)
}
if _, err := p.Read(b); err == nil || !errors.Is(err, ErrReadTimeout) {
if _, err := p.Read(b); !errors.Is(err, ErrReadTimeout) {
t.Fatalf("want read timeout got: %v", err)
}
})
}
func TestLimit(t *testing.T) {
@@ -117,4 +116,8 @@ func TestLimit(t *testing.T) {
} else if n != 1 {
t.Errorf("Read(%q): n=%d want 1", string(b), n)
}
if err := <-errCh; err != nil {
t.Error(err)
}
}

View File

@@ -36,9 +36,6 @@ type Header interface {
// purpose of computing length and checksum fields. Marshal
// implementations must not allocate memory.
Marshal(buf []byte) error
// ToResponse transforms the header into one for a response packet.
// For instance, this swaps the source and destination IPs.
ToResponse()
}
// Generate generates a new packet with the given Header and

View File

@@ -24,6 +24,17 @@ const (
TCP IPProto = 0x06
UDP IPProto = 0x11
// TSMP is the Tailscale Message Protocol (our ICMP-ish
// thing), an IP protocol used only between Tailscale nodes
// (still encrypted by WireGuard) that communicates why things
// failed, etc.
//
// Proto number 99 is reserved for "any private encryption
// scheme". We never accept these from the host OS stack nor
// send them to the host network stack. It's only used between
// nodes.
TSMP IPProto = 99
// Fragment represents any non-first IP fragment, for which we
// don't have the sub-protocol header (and therefore can't
// figure out what the sub-protocol is).
@@ -47,6 +58,8 @@ func (p IPProto) String() string {
return "UDP"
case TCP:
return "TCP"
case TSMP:
return "TSMP"
default:
return "Unknown"
}

View File

@@ -204,6 +204,10 @@ func (q *Parsed) decode4(b []byte) {
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
return
case TSMP:
// Inter-tailscale messages.
q.dataofs = q.subofs
return
default:
q.IPProto = Unknown
return
@@ -291,6 +295,10 @@ func (q *Parsed) decode6(b []byte) {
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
case TSMP:
// Inter-tailscale messages.
q.dataofs = q.subofs
return
default:
q.IPProto = Unknown
return

View File

@@ -274,7 +274,38 @@ var igmpPacketDecode = Parsed{
Dst: mustIPPort("224.0.0.251:0"),
}
func TestParsed(t *testing.T) {
var ipv4TSMPBuffer = []byte{
// IPv4 header:
0x45, 0x00,
0x00, 0x1b, // 20 + 7 bytes total
0x00, 0x00, // ID
0x00, 0x00, // Fragment
0x40, // TTL
byte(TSMP),
0x5f, 0xc3, // header checksum (wrong here)
// source IP:
0x64, 0x5e, 0x0c, 0x0e,
// dest IP:
0x64, 0x4a, 0x46, 0x03,
byte(TSMPTypeRejectedConn),
byte(TCP),
byte(RejectedDueToACLs),
0x00, 123, // src port
0x00, 80, // dst port
}
var ipv4TSMPDecode = Parsed{
b: ipv4TSMPBuffer,
subofs: 20,
dataofs: 20,
length: 27,
IPVersion: 4,
IPProto: TSMP,
Src: mustIPPort("100.94.12.14:0"),
Dst: mustIPPort("100.74.70.3:0"),
}
func TestParsedString(t *testing.T) {
tests := []struct {
name string
qdecode Parsed
@@ -288,6 +319,7 @@ func TestParsed(t *testing.T) {
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
{"unknown", unknownPacketDecode, "Unknown{???}"},
{"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"},
}
for _, tt := range tests {
@@ -324,6 +356,7 @@ func TestDecode(t *testing.T) {
{"igmp", igmpPacketBuffer, igmpPacketDecode},
{"unknown", unknownPacketBuffer, unknownPacketDecode},
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
}
for _, tt := range tests {
@@ -331,7 +364,7 @@ func TestDecode(t *testing.T) {
var got Parsed
got.Decode(tt.buf)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
t.Errorf("mismatch\n got: %s %#v\nwant: %s %#v", got.String(), got, tt.want.String(), tt.want)
}
})
}
@@ -416,9 +449,16 @@ func TestMarshalResponse(t *testing.T) {
icmpHeader := icmp4RequestDecode.ICMP4Header()
udpHeader := udp4RequestDecode.UDP4Header()
type HeaderToResponser interface {
Header
// ToResponse transforms the header into one for a response packet.
// For instance, this swaps the source and destination IPs.
ToResponse()
}
tests := []struct {
name string
header Header
header HeaderToResponser
want []byte
}{
{"icmp", &icmpHeader, icmp4ReplyBuffer},

140
net/packet/tsmp.go Normal file
View File

@@ -0,0 +1,140 @@
// 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.
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
// Tailscale-specific messages between nodes. It uses IP protocol 99
// (reserved for "any private encryption scheme") within
// Wireguard's normal encryption between peers and never hits the host
// network stack.
package packet
import (
"encoding/binary"
"errors"
"fmt"
"inet.af/netaddr"
"tailscale.com/net/flowtrack"
)
// TailscaleRejectedHeader is a TSMP message that says that one
// Tailscale node has rejected the connection from another. Unlike a
// TCP RST, this includes a reason.
//
// On the wire, after the IP header, it's currently 7 bytes:
// * '!'
// * IPProto byte (IANA protocol number: TCP or UDP)
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
// * srcPort big endian uint16
// * dstPort big endian uint16
//
// In the future it might also accept 16 byte IP flow src/dst IPs
// after the header, if they're different than the IP-level ones.
type TailscaleRejectedHeader struct {
IPSrc netaddr.IP // IPv4 or IPv6 header's src IP
IPDst netaddr.IP // IPv4 or IPv6 header's dst IP
Src netaddr.IPPort // rejected flow's src
Dst netaddr.IPPort // rejected flow's dst
Proto IPProto // proto that was rejected (TCP or UDP)
Reason TailscaleRejectReason // why the connection was rejected
}
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
}
func (rh TailscaleRejectedHeader) String() string {
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
}
type TSMPType uint8
const (
TSMPTypeRejectedConn TSMPType = '!'
)
type TailscaleRejectReason byte
const (
RejectedDueToACLs TailscaleRejectReason = 'A'
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
)
func (r TailscaleRejectReason) String() string {
switch r {
case RejectedDueToACLs:
return "acl"
case RejectedDueToShieldsUp:
return "shields"
}
return fmt.Sprintf("0x%02x", byte(r))
}
func (h TailscaleRejectedHeader) Len() int {
var ipHeaderLen int
if h.IPSrc.Is4() {
ipHeaderLen = ip4HeaderLength
} else if h.IPSrc.Is6() {
ipHeaderLen = ip6HeaderLength
}
return ipHeaderLen +
1 + // TSMPType byte
1 + // IPProto byte
1 + // TailscaleRejectReason byte
2*2 // 2 uint16 ports
}
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
if h.Src.IP.Is4() {
iph := IP4Header{
IPProto: TSMP,
Src: h.IPSrc,
Dst: h.IPDst,
}
iph.Marshal(buf)
buf = buf[ip4HeaderLength:]
} else if h.Src.IP.Is6() {
iph := IP6Header{
IPProto: TSMP,
Src: h.IPSrc,
Dst: h.IPDst,
}
iph.Marshal(buf)
buf = buf[ip6HeaderLength:]
} else {
return errors.New("bogus src IP")
}
buf[0] = byte(TSMPTypeRejectedConn)
buf[1] = byte(h.Proto)
buf[2] = byte(h.Reason)
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
return nil
}
// AsTailscaleRejectedHeader parses pp as an incoming rejection
// connection TSMP message.
//
// ok reports whether pp was a valid TSMP rejection packet.
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
p := pp.Payload()
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
return
}
return TailscaleRejectedHeader{
Proto: IPProto(p[1]),
Reason: TailscaleRejectReason(p[2]),
IPSrc: pp.Src.IP,
IPDst: pp.Dst.IP,
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
}, true
}

63
net/packet/tsmp_test.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 packet
import (
"testing"
"inet.af/netaddr"
)
func TestTailscaleRejectedHeader(t *testing.T) {
tests := []struct {
h TailscaleRejectedHeader
wantStr string
}{
{
h: TailscaleRejectedHeader{
IPSrc: netaddr.MustParseIP("5.5.5.5"),
IPDst: netaddr.MustParseIP("1.2.3.4"),
Src: netaddr.MustParseIPPort("1.2.3.4:567"),
Dst: netaddr.MustParseIPPort("5.5.5.5:443"),
Proto: TCP,
Reason: RejectedDueToACLs,
},
wantStr: "TSMP-reject-flow{TCP 1.2.3.4:567 > 5.5.5.5:443}: acl",
},
{
h: TailscaleRejectedHeader{
IPSrc: netaddr.MustParseIP("2::2"),
IPDst: netaddr.MustParseIP("1::1"),
Src: netaddr.MustParseIPPort("[1::1]:567"),
Dst: netaddr.MustParseIPPort("[2::2]:443"),
Proto: UDP,
Reason: RejectedDueToShieldsUp,
},
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
},
}
for i, tt := range tests {
gotStr := tt.h.String()
if gotStr != tt.wantStr {
t.Errorf("%v. String = %q; want %q", i, gotStr, tt.wantStr)
continue
}
pkt := make([]byte, tt.h.Len())
tt.h.Marshal(pkt)
var p Parsed
p.Decode(pkt)
t.Logf("Parsed: %+v", p)
t.Logf("Parsed: %s", p.String())
back, ok := p.AsTailscaleRejectedHeader()
if !ok {
t.Errorf("%v. %q (%02x) didn't parse back", i, gotStr, pkt)
continue
}
if back != tt.h {
t.Errorf("%v. %q parsed back as %q", i, tt.h, back)
}
}
}

View File

@@ -59,15 +59,58 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
return nil, 0, fmt.Errorf("%v: address already in use", path)
}
_ = os.Remove(path)
os.MkdirAll(filepath.Dir(path), 0755) // best effort
perm := socketPermissionsForOS()
sockDir := filepath.Dir(path)
if _, err := os.Stat(sockDir); os.IsNotExist(err) {
os.MkdirAll(sockDir, 0755) // best effort
// If we're on a platform where we want the socket
// world-readable, open up the permissions on the
// just-created directory too, in case a umask ate
// it. This primarily affects running tailscaled by
// hand as root in a shell, as there is no umask when
// running under systemd.
if perm == 0666 {
if fi, err := os.Stat(sockDir); err == nil && fi.Mode()&0077 == 0 {
if err := os.Chmod(sockDir, 0755); err != nil {
log.Print(err)
}
}
}
}
pipe, err := net.Listen("unix", path)
if err != nil {
return nil, 0, err
}
os.Chmod(path, 0600)
os.Chmod(path, perm)
return pipe, 0, err
}
// socketPermissionsForOS returns the permissions to use for the
// tailscaled.sock.
func socketPermissionsForOS() os.FileMode {
if runtime.GOOS == "linux" {
// On Linux, the ipn/ipnserver package looks at the Unix peer creds
// and only permits read-only actions from non-root users, so we want
// this opened up wider.
//
// TODO(bradfitz): unify this all one in place probably, moving some
// of ipnserver (which does much of the "safe" bits) here. Maybe
// instead of net.Listener, we should return a type that returns
// an identity in addition to a net.Conn? (returning a wrapped net.Conn
// would surprise downstream callers probably)
//
// TODO(bradfitz): if OpenBSD and FreeBSD do the equivalent peercreds
// stuff that's in ipn/ipnserver/conn_ucred.go, they should also
// return 0666 here.
return 0666
}
// Otherwise, root only.
return 0600
}
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
// which is necessarily running within the macOS App Sandbox. Our
// little dance to connect a regular user binary to the sandboxed

View File

@@ -20,6 +20,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/util/dnsname"
)
// CurrentMapRequestVersion is the current MapRequest.Version value.
@@ -32,7 +33,10 @@ import (
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
const CurrentMapRequestVersion = 9
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
const CurrentMapRequestVersion = 10
type StableID string
type ID int64
@@ -54,6 +58,12 @@ func (u NodeID) IsZero() bool {
return u == 0
}
type StableNodeID StableID
func (u StableNodeID) IsZero() bool {
return u == ""
}
type GroupID ID
func (u GroupID) IsZero() bool {
@@ -147,8 +157,9 @@ type UserProfile struct {
}
type Node struct {
ID NodeID
Name string // DNS
ID NodeID
StableID StableNodeID
Name string // DNS
// User is the user who created the node. If ACL tags are in
// use for the node then it doesn't reflect the ACL identity
@@ -173,6 +184,98 @@ type Node struct {
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
// The following three computed fields hold the various names that can
// be used for this node in UIs. They are populated from controlclient
// (not from control) by calling node.InitDisplayNames. These can be
// used directly or accessed via node.DisplayName or node.DisplayNames.
ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
}
// DisplayName returns the user-facing name for a node which should
// be shown in client UIs.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, the hostname is
// never included in the return value.
//
// Return value is either either "Name" or "Name (Hostname)", where
// Name is the node's MagicDNS base name (for normal non-shared-in
// nodes), FQDN (without trailing dot, for shared-in nodes), or
// Hostname (if no MagicDNS). Hostname is only included in the
// return value if it varies from Name and forOwner is provided true.
//
// DisplayName is only valid if InitDisplayNames has been called.
func (n *Node) DisplayName(forOwner bool) string {
if forOwner {
return n.ComputedNameWithHost
}
return n.ComputedName
}
// DisplayName returns the decomposed user-facing name for a node.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, hostIfDifferent
// is always returned empty.
//
// Return value name is the node's primary name, populated with the
// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
// (without trailing dot, for shared-in nodes), or Hostname (if no
// MagicDNS).
//
// Return value hostIfDifferent, when non-empty, is the node's
// hostname. hostIfDifferent is only populated when the hostname
// varies from name and forOwner is provided as true.
//
// DisplayNames is only valid if InitDisplayNames has been called.
func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
if forOwner {
return n.ComputedName, n.computedHostIfDifferent
}
return n.ComputedName, ""
}
// InitDisplayNames computes and populates n's display name
// fields: n.ComputedName, n.computedHostIfDifferent, and
// n.ComputedNameWithHost.
func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
dnsName := n.Name
if dnsName != "" {
dnsName = strings.TrimRight(dnsName, ".")
if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, networkMagicDNSSuffix) {
dnsName = dnsName[:i]
}
}
name := dnsName
hostIfDifferent := n.Hostinfo.Hostname
if strings.EqualFold(name, hostIfDifferent) {
hostIfDifferent = ""
}
if name == "" {
if hostIfDifferent != "" {
name = hostIfDifferent
hostIfDifferent = ""
} else {
name = n.Key.String()
}
}
var nameWithHost string
if hostIfDifferent != "" {
nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
} else {
nameWithHost = name
}
n.ComputedName = name
n.computedHostIfDifferent = hostIfDifferent
n.ComputedNameWithHost = nameWithHost
}
type MachineStatus int
@@ -531,8 +634,6 @@ type MapRequest struct {
// Current DebugFlags values are:
// * "warn-ip-forwarding-off": client is trying to be a subnet
// router but their IP forwarding is broken.
// * "v6-overlay": IPv6 development flag to have control send
// v6 node addrs
// * "minimize-netmap": have control minimize the netmap, removing
// peers that are unreachable per ACLS.
DebugFlags []string `json:",omitempty"`
@@ -638,6 +739,11 @@ type MapResponse struct {
// PeersRemoved are the NodeIDs that are no longer in the peer list.
PeersRemoved []NodeID `json:",omitempty"`
// PeerSeenChange contains information on how to update peers' LastSeen
// times. If the value is false, the peer is gone. If the value is true,
// the LastSeen time is now. Absent means unchanged.
PeerSeenChange map[NodeID]bool `json:",omitempty"`
// DNS is the same as DNSConfig.Nameservers.
//
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
@@ -781,6 +887,7 @@ func (n *Node) Equal(n2 *Node) bool {
}
return n != nil && n2 != nil &&
n.ID == n2.ID &&
n.StableID == n2.StableID &&
n.Name == n2.Name &&
n.User == n2.User &&
n.Sharer == n2.Sharer &&
@@ -795,7 +902,10 @@ func (n *Node) Equal(n2 *Node) bool {
n.Hostinfo.Equal(&n2.Hostinfo) &&
n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) &&
n.MachineAuthorized == n2.MachineAuthorized
n.MachineAuthorized == n2.MachineAuthorized &&
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost
}
func eqStrings(a, b []string) bool {
@@ -825,3 +935,9 @@ func eqCIDRs(a, b []netaddr.IPPrefix) bool {
func eqTimePtr(a, b *time.Time) bool {
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *Node
UserProfile *UserProfile
}

View File

@@ -61,23 +61,27 @@ func (src *Node) Clone() *Node {
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _NodeNeedsRegeneration = Node(struct {
ID NodeID
Name string
User UserID
Sharer UserID
Key NodeKey
KeyExpiry time.Time
Machine MachineKey
DiscoKey DiscoKey
Addresses []netaddr.IPPrefix
AllowedIPs []netaddr.IPPrefix
Endpoints []string
DERP string
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time
KeepAlive bool
MachineAuthorized bool
ID NodeID
StableID StableNodeID
Name string
User UserID
Sharer UserID
Key NodeKey
KeyExpiry time.Time
Machine MachineKey
DiscoKey DiscoKey
Addresses []netaddr.IPPrefix
AllowedIPs []netaddr.IPPrefix
Endpoints []string
DERP string
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time
KeepAlive bool
MachineAuthorized bool
ComputedName string
computedHostIfDifferent string
ComputedNameWithHost string
}{})
// Clone makes a deep copy of Hostinfo.

View File

@@ -189,10 +189,11 @@ func TestHostinfoEqual(t *testing.T) {
func TestNodeEqual(t *testing.T) {
nodeHandles := []string{
"ID", "Name", "User", "Sharer",
"ID", "StableID", "Name", "User", "Sharer",
"Key", "KeyExpiry", "Machine", "DiscoKey",
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
}
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
@@ -229,6 +230,31 @@ func TestNodeEqual(t *testing.T) {
&Node{},
true,
},
{
&Node{},
&Node{},
true,
},
{
&Node{ID: 1},
&Node{},
false,
},
{
&Node{ID: 1},
&Node{ID: 1},
true,
},
{
&Node{StableID: "node-abcd"},
&Node{},
false,
},
{
&Node{StableID: "node-abcd"},
&Node{StableID: "node-abcd"},
true,
},
{
&Node{User: 0},
&Node{User: 1},

44
tstime/jitter.go Normal file
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 tstime
import (
crand "crypto/rand"
"encoding/binary"
"math/rand"
"sync"
"time"
)
// crandSource is a rand.Source64 that gets its numbers from
// crypto/rand.Reader.
type crandSource struct{ sync.Mutex }
var _ rand.Source64 = (*crandSource)(nil)
func (s *crandSource) Int63() int64 { return int64(s.Uint64() >> 1) }
func (s *crandSource) Uint64() uint64 {
s.Lock()
defer s.Unlock()
var buf [8]byte
crand.Read(buf[:])
return binary.BigEndian.Uint64(buf[:])
}
func (*crandSource) Seed(seed int64) {} // nope
var durRand = rand.New(new(crandSource))
// RandomDurationBetween returns a random duration in range [min,max).
// If panics if max < min.
func RandomDurationBetween(min, max time.Duration) time.Duration {
diff := max - min
if diff == 0 {
return min
}
ns := durRand.Int63n(int64(diff))
return min + time.Duration(ns)
}

23
tstime/jitter_test.go Normal file
View File

@@ -0,0 +1,23 @@
// 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 tstime
import (
"testing"
"time"
)
func TestRandomDurationBetween(t *testing.T) {
if got := RandomDurationBetween(1, 1); got != 1 {
t.Errorf("between 1 and 1 = %v; want 1", int64(got))
}
const min = 1 * time.Second
const max = 10 * time.Second
for i := 0; i < 500; i++ {
if got := RandomDurationBetween(min, max); got < min || got >= max {
t.Fatalf("%v (%d) out of range", got, got)
}
}
}

View File

@@ -23,8 +23,9 @@ import (
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/metrics"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
)
@@ -81,8 +82,11 @@ func AllowDebugAccess(r *http.Request) bool {
if err != nil {
return false
}
ip := net.ParseIP(ipStr)
if interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return false
}
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
return true
}
if r.Method == "GET" {

View File

@@ -64,9 +64,9 @@ type limitData struct {
var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
// rateFreePrefix are format string prefixes that are exempt from rate limiting.
// rateFree are format string substrings that are exempt from rate limiting.
// Things should not be added to this unless they're already limited otherwise.
var rateFreePrefix = []string{
var rateFree = []string{
"magicsock: disco: ",
"magicsock: CreateEndpoint:",
}
@@ -93,8 +93,8 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
)
judge := func(format string) verdict {
for _, pfx := range rateFreePrefix {
if strings.HasPrefix(format, pfx) {
for _, sub := range rateFree {
if strings.Contains(format, sub) {
return allow
}
}
@@ -132,7 +132,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
logf(format, args...)
case warn:
// For the warning, log the specific format string
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, fmt.Sprintf(format, args...))
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, strings.TrimSpace(fmt.Sprintf(format, args...)))
}
}
}
@@ -192,3 +192,27 @@ func Filtered(logf Logf, allow func(s string) bool) Logf {
logf(format, args...)
}
}
// LogfCloser wraps logf to create a logger that can be closed.
// Calling close makes all future calls to newLogf into no-ops.
func LogfCloser(logf Logf) (newLogf Logf, close func()) {
var (
mu sync.Mutex
closed bool
)
close = func() {
mu.Lock()
defer mu.Unlock()
closed = true
}
newLogf = func(msg string, args ...interface{}) {
mu.Lock()
if closed {
mu.Unlock()
return
}
mu.Unlock()
logf(msg, args...)
}
return newLogf, close
}

View File

@@ -39,6 +39,8 @@ type Filter struct {
// to an outbound connection that this node made, even if those
// incoming packets don't get accepted by matches above.
state *filterState
shieldsUp bool
}
// filterState is a state cache of past seen packets.
@@ -54,15 +56,18 @@ const lruMax = 512
type Response int
const (
Drop Response = iota // do not continue processing packet.
Accept // continue processing packet.
noVerdict // no verdict yet, continue running filter
Drop Response = iota // do not continue processing packet.
DropSilently // do not continue processing packet, but also don't log
Accept // continue processing packet.
noVerdict // no verdict yet, continue running filter
)
func (r Response) String() string {
switch r {
case Drop:
return "Drop"
case DropSilently:
return "DropSilently"
case Accept:
return "Accept"
case noVerdict:
@@ -72,6 +77,10 @@ func (r Response) String() string {
}
}
func (r Response) IsDrop() bool {
return r == Drop || r == DropSilently
}
// RunFlags controls the filter's debug log verbosity at runtime.
type RunFlags int
@@ -123,6 +132,20 @@ func NewAllowNone(logf logger.Logf) *Filter {
return New(nil, nil, nil, logf)
}
// NewShieldsUpFilter returns a packet filter that rejects incoming connections.
//
// If shareStateWith is non-nil, the returned filter shares state with the previous one,
// as long as the previous one was also a shields up filter.
func NewShieldsUpFilter(localNets []netaddr.IPPrefix, shareStateWith *Filter, logf logger.Logf) *Filter {
// Don't permit sharing state with a prior filter that wasn't a shields-up filter.
if shareStateWith != nil && !shareStateWith.shieldsUp {
shareStateWith = nil
}
f := New(nil, localNets, shareStateWith, logf)
f.shieldsUp = true
return f
}
// New creates a new packet filter. The filter enforces that incoming
// packets must be destined to an IP in localNets, and must be allowed
// by matches. If shareStateWith is non-nil, the returned filter
@@ -253,6 +276,10 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
return f.RunIn(pkt, 0)
}
// ShieldsUp reports whether this is a "shields up" (block everything
// incoming) filter.
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
// RunIn determines whether this node is allowed to receive q from a
// Tailscale peer.
func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
@@ -339,6 +366,8 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
if f.matches4.match(q) {
return Accept, "udp ok"
}
case packet.TSMP:
return Accept, "tsmp ok"
default:
return Drop, "Unknown proto"
}

View File

@@ -12,6 +12,8 @@ import (
"tailscale.com/net/packet"
)
//go:generate go run tailscale.com/cmd/cloner --type=Match --output=match_clone.go
// PortRange is a range of TCP and UDP ports.
type PortRange struct {
First, Last uint16 // inclusive

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale.com/cmd/cloner -type Match; DO NOT EDIT.
package filter
import (
"inet.af/netaddr"
)
// Clone makes a deep copy of Match.
// The result aliases no memory with the original.
func (src *Match) Clone() *Match {
if src == nil {
return nil
}
dst := new(Match)
*dst = *src
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type Match
var _MatchNeedsRegeneration = Match(struct {
Dsts []NetPortRange
Srcs []netaddr.IPPrefix
}{})

View File

@@ -30,9 +30,16 @@ import (
"tailscale.com/types/wgkey"
)
var errNoDestinations = errors.New("magicsock: no destinations")
var (
errNoDestinations = errors.New("magicsock: no destinations")
errDisabled = errors.New("magicsock: legacy networking disabled")
)
func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.Endpoint, error) {
if c.disableLegacy {
return nil, errDisabled
}
a := &addrSet{
Logf: c.logf,
publicKey: pk,
@@ -78,6 +85,10 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
}
func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, packet []byte) conn.Endpoint {
if c.disableLegacy {
return nil
}
// Pre-disco: look up their addrSet.
if as, ok := c.addrsByUDP[ipp]; ok {
as.updateDst(addr)
@@ -139,6 +150,10 @@ func (c *Conn) resetAddrSetStatesLocked() {
}
func (c *Conn) sendAddrSet(b []byte, as *addrSet) error {
if c.disableLegacy {
return errDisabled
}
var addrBuf [8]netaddr.IPPort
dsts, roamAddr := as.appendDests(addrBuf[:0], b)
@@ -423,8 +438,7 @@ func (a *addrSet) DstToBytes() []byte {
return packIPPort(a.dst())
}
func (a *addrSet) DstToString() string {
dst := a.dst()
return dst.String()
return a.Addrs()
}
func (a *addrSet) DstIP() net.IP {
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
@@ -433,10 +447,6 @@ func (a *addrSet) SrcIP() net.IP { return nil }
func (a *addrSet) SrcToString() string { return "" }
func (a *addrSet) ClearSrc() {}
func (a *addrSet) UpdateDst(new *net.UDPAddr) error {
return nil
}
// updateDst records receipt of a packet from new. This is used to
// potentially update the transmit address used for this addrSet.
func (a *addrSet) updateDst(new *net.UDPAddr) error {

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ import (
"crypto/tls"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
@@ -21,6 +20,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"unsafe"
@@ -46,6 +46,7 @@ import (
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/tstun"
"tailscale.com/wgengine/wglog"
)
func init() {
@@ -127,12 +128,13 @@ type magicStack struct {
tun *tuntest.ChannelTUN // TUN device to send/receive packets
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
dev *device.Device // the wireguard-go Device that connects the previous things
wgLogger *wglog.Logger // wireguard-go log wrapper
}
// newMagicStack builds and initializes an idle magicsock and
// friends. You need to call conn.SetNetworkMap and dev.Reconfig
// before anything interesting happens.
func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap) *magicStack {
func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, disableLegacy bool) *magicStack {
t.Helper()
privateKey, err := wgkey.NewPrivate()
@@ -147,7 +149,8 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
EndpointsFunc: func(eps []string) {
epCh <- eps
},
SimulatedNetwork: l != nettype.Std{},
SimulatedNetwork: l != nettype.Std{},
DisableLegacyNetworking: disableLegacy,
})
if err != nil {
t.Fatalf("constructing magicsock: %v", err)
@@ -162,12 +165,9 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
tsTun := tstun.WrapTUN(logf, tun.TUN())
tsTun.SetFilter(filter.NewAllowAllForTest(logf))
wgLogger := wglog.NewLogger(logf)
dev := device.NewDevice(tsTun, &device.DeviceOptions{
Logger: &device.Logger{
Debug: logger.StdLogger(logf),
Info: logger.StdLogger(logf),
Error: logger.StdLogger(logf),
},
Logger: wgLogger.DeviceLogger,
CreateEndpoint: conn.CreateEndpoint,
CreateBind: conn.CreateBind,
SkipBindUpdate: true,
@@ -190,9 +190,15 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
tun: tun,
tsTun: tsTun,
dev: dev,
wgLogger: wgLogger,
}
}
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
s.wgLogger.SetPeers(cfg.Peers)
return s.dev.Reconfig(cfg)
}
func (s *magicStack) String() string {
pub := s.Public()
return pub.ShortString()
@@ -293,7 +299,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
// blow up. Shouldn't happen anyway.
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
}
if err := m.dev.Reconfig(wg); err != nil {
if err := m.Reconfig(wg); err != nil {
panic(fmt.Sprintf("device reconfig failed: %v", err))
}
}
@@ -340,9 +346,10 @@ func TestNewConn(t *testing.T) {
port := pickPort(t)
conn, err := NewConn(Options{
Port: port,
EndpointsFunc: epFunc,
Logf: t.Logf,
Port: port,
EndpointsFunc: epFunc,
Logf: t.Logf,
DisableLegacyNetworking: true,
})
if err != nil {
t.Fatal(err)
@@ -355,7 +362,7 @@ func TestNewConn(t *testing.T) {
go func() {
var pkt [64 << 10]byte
for {
_, _, _, err := conn.ReceiveIPv4(pkt[:])
_, _, err := conn.ReceiveIPv4(pkt[:])
if err != nil {
return
}
@@ -516,8 +523,9 @@ func TestDeviceStartStop(t *testing.T) {
defer rc.Assert(t)
conn, err := NewConn(Options{
EndpointsFunc: func(eps []string) {},
Logf: t.Logf,
EndpointsFunc: func(eps []string) {},
Logf: t.Logf,
DisableLegacyNetworking: true,
})
if err != nil {
t.Fatal(err)
@@ -527,11 +535,7 @@ func TestDeviceStartStop(t *testing.T) {
tun := tuntest.NewChannelTUN()
dev := device.NewDevice(tun.TUN(), &device.DeviceOptions{
Logger: &device.Logger{
Debug: logger.StdLogger(t.Logf),
Info: logger.StdLogger(t.Logf),
Error: logger.StdLogger(t.Logf),
},
Logger: wglog.NewLogger(t.Logf).DeviceLogger,
CreateEndpoint: conn.CreateEndpoint,
CreateBind: conn.CreateBind,
SkipBindUpdate: true,
@@ -540,93 +544,6 @@ func TestDeviceStartStop(t *testing.T) {
dev.Close()
}
// A context used in TestConnClosing() which seeks to test that code which calls
// Err() to see if a connection is already being closed does not then proceed to
// try to acquire the mutex, as this would lead to deadlock. When Err() is called
// this context acquires the lock itself, in order to force a deadlock (and test
// failure on timeout).
type testConnClosingContext struct {
parent context.Context
mu *sync.Mutex
}
func (c *testConnClosingContext) Deadline() (deadline time.Time, ok bool) {
d, o := c.parent.Deadline()
return d, o
}
func (c *testConnClosingContext) Done() <-chan struct{} {
return c.parent.Done()
}
func (c *testConnClosingContext) Err() error {
// Deliberately deadlock if anything grabs the lock after checking Err()
c.mu.Lock()
return errors.New("testConnClosingContext error")
}
func (c *testConnClosingContext) Value(key interface{}) interface{} {
return c.parent.Value(key)
}
func (*testConnClosingContext) String() string {
return "testConnClosingContext"
}
func TestConnClosing(t *testing.T) {
privateKey, err := wgkey.NewPrivate()
if err != nil {
t.Fatalf("generating private key: %v", err)
}
epCh := make(chan []string, 100)
conn, err := NewConn(Options{
Logf: t.Logf,
PacketListener: nettype.Std{},
EndpointsFunc: func(eps []string) {
epCh <- eps
},
SimulatedNetwork: false,
})
if err != nil {
t.Fatalf("constructing magicsock: %v", err)
}
derpMap, cleanup := runDERPAndStun(t, t.Logf, nettype.Std{}, netaddr.IPv4(127, 0, 3, 1))
defer cleanup()
// The point of this test case is to exercise handling in derpWriteChanOfAddr() which
// returns early if connCtx.Err() returns non-nil, to avoid a deadlock on conn.mu.
// We swap in a context which always returns an error, and deliberately grabs the lock
// to cause a deadlock if magicsock.go tries to acquire the lock after calling Err().
closingCtx := testConnClosingContext{parent: conn.connCtx, mu: &conn.mu}
conn.connCtx = &closingCtx
conn.Start()
conn.SetDERPMap(derpMap)
if err := conn.SetPrivateKey(privateKey); err != nil {
t.Fatalf("setting private key in magicsock: %v", err)
}
tun := tuntest.NewChannelTUN()
tsTun := tstun.WrapTUN(t.Logf, tun.TUN())
tsTun.SetFilter(filter.NewAllowAllForTest(t.Logf))
dev := device.NewDevice(tsTun, &device.DeviceOptions{
Logger: &device.Logger{
Debug: logger.StdLogger(t.Logf),
Info: logger.StdLogger(t.Logf),
Error: logger.StdLogger(t.Logf),
},
CreateEndpoint: conn.CreateEndpoint,
CreateBind: conn.CreateBind,
SkipBindUpdate: true,
})
dev.Up()
conn.WaitReady(t)
// We don't assert any failures within the test itself. If derpWriteChanOfAddr tries to
// grab the lock it will deadlock, and conn.WaitReady(t) will call t.Fatal() after timeout.
// (verified by deliberately breaking derpWriteChanOfAddr)
}
// Exercise a code path in sendDiscoMessage if the connection has been closed.
func TestConnClosed(t *testing.T) {
mstun := &natlab.Machine{Name: "stun"}
@@ -646,12 +563,15 @@ func TestConnClosed(t *testing.T) {
stunIP: sif.V4(),
}
derpMap, cleanup := runDERPAndStun(t, t.Logf, d.stun, d.stunIP)
logf, closeLogf := logger.LogfCloser(t.Logf)
defer closeLogf()
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
defer cleanup()
ms1 := newMagicStack(t, logger.WithPrefix(t.Logf, "conn1: "), d.m1, derpMap)
ms1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap, true)
defer ms1.Close()
ms2 := newMagicStack(t, logger.WithPrefix(t.Logf, "conn2: "), d.m2, derpMap)
ms2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap, true)
defer ms2.Close()
cleanup = meshStacks(t.Logf, []*magicStack{ms1, ms2})
@@ -925,18 +845,20 @@ func testActiveDiscovery(t *testing.T, d *devices) {
setT(t)
start := time.Now()
logf := func(msg string, args ...interface{}) {
wlogf := func(msg string, args ...interface{}) {
t.Helper()
msg = fmt.Sprintf("%s: %s", time.Since(start).Truncate(time.Microsecond), msg)
tlogf(msg, args...)
}
logf, closeLogf := logger.LogfCloser(wlogf)
defer closeLogf()
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
defer cleanup()
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap)
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap, true)
defer m1.Close()
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap)
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap, true)
defer m2.Close()
cleanup = meshStacks(logf, []*magicStack{m1, m2})
@@ -983,14 +905,17 @@ func testTwoDevicePing(t *testing.T, d *devices) {
// This gets reassigned inside every test, so that the connections
// all log using the "current" t.Logf function. Sigh.
logf, setT := makeNestable(t)
nestedLogf, setT := makeNestable(t)
logf, closeLogf := logger.LogfCloser(nestedLogf)
defer closeLogf()
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
defer cleanup()
m1 := newMagicStack(t, logf, d.m1, derpMap)
m1 := newMagicStack(t, logf, d.m1, derpMap, false)
defer m1.Close()
m2 := newMagicStack(t, logf, d.m2, derpMap)
m2 := newMagicStack(t, logf, d.m2, derpMap, false)
defer m2.Close()
addrs := []netaddr.IPPort{
@@ -999,10 +924,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
}
cfgs := makeConfigs(t, addrs)
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
if err := m1.Reconfig(&cfgs[0]); err != nil {
t.Fatal(err)
}
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
if err := m2.Reconfig(&cfgs[1]); err != nil {
t.Fatal(err)
}
@@ -1067,7 +992,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
t.Run("no-op dev1 reconfig", func(t *testing.T) {
setT(t)
defer setT(outerT)
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
if err := m1.Reconfig(&cfgs[0]); err != nil {
t.Fatal(err)
}
ping1(t)
@@ -1144,10 +1069,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
ep1 := cfgs[1].Peers[0].Endpoints
ep1 = derpEp + "," + ep1
cfgs[1].Peers[0].Endpoints = ep1
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
if err := m1.Reconfig(&cfgs[0]); err != nil {
t.Fatal(err)
}
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
if err := m2.Reconfig(&cfgs[1]); err != nil {
t.Fatal(err)
}
@@ -1160,10 +1085,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
// Disable real route.
cfgs[0].Peers[0].Endpoints = derpEp
cfgs[1].Peers[0].Endpoints = derpEp
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
if err := m1.Reconfig(&cfgs[0]); err != nil {
t.Fatal(err)
}
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
if err := m2.Reconfig(&cfgs[1]); err != nil {
t.Fatal(err)
}
time.Sleep(250 * time.Millisecond) // TODO remove
@@ -1189,10 +1114,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
if ep2 := cfgs[1].Peers[0].Endpoints; len(ep2) != 1 {
t.Errorf("unexpected peer endpoints in dev2: %v", ep2)
}
if err := m2.dev.Reconfig(&cfgs[1]); err != nil {
if err := m2.Reconfig(&cfgs[1]); err != nil {
t.Fatal(err)
}
if err := m1.dev.Reconfig(&cfgs[0]); err != nil {
if err := m1.Reconfig(&cfgs[0]); err != nil {
t.Fatal(err)
}
// Dear future human debugging a test failure here: this test is
@@ -1469,7 +1394,7 @@ func stringifyConfig(cfg wgcfg.Config) string {
return string(j)
}
func TestDiscoEndpointAlignment(t *testing.T) {
func Test32bitAlignment(t *testing.T) {
var de discoEndpoint
off := unsafe.Offsetof(de.lastRecvUnixAtomic)
if off%8 != 0 {
@@ -1481,6 +1406,8 @@ func TestDiscoEndpointAlignment(t *testing.T) {
if de.isFirstRecvActivityInAwhile() {
t.Error("expected false on second call")
}
var c Conn
atomic.AddInt64(&c.derpRecvCountAtomic, 1)
}
func BenchmarkReceiveFrom(b *testing.B) {
@@ -1491,6 +1418,7 @@ func BenchmarkReceiveFrom(b *testing.B) {
EndpointsFunc: func(eps []string) {
b.Logf("endpoints: %q", eps)
},
DisableLegacyNetworking: true,
})
if err != nil {
b.Fatal(err)
@@ -1503,6 +1431,21 @@ func BenchmarkReceiveFrom(b *testing.B) {
}
defer sendConn.Close()
// Give conn just enough state that it'll recognize sendConn as a
// valid peer and not fall through to the legacy magicsock
// codepath.
discoKey := tailcfg.DiscoKey{31: 1}
conn.SetNetworkMap(&controlclient.NetworkMap{
Peers: []*tailcfg.Node{
{
DiscoKey: discoKey,
Endpoints: []string{sendConn.LocalAddr().String()},
},
},
})
conn.CreateEndpoint([32]byte{1: 1}, "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
conn.addValidDiscoPathForTest(discoKey, netaddr.MustParseIPPort(sendConn.LocalAddr().String()))
var dstAddr net.Addr = conn.pconn4.LocalAddr()
sendBuf := make([]byte, 1<<10)
for i := range sendBuf {
@@ -1514,13 +1457,12 @@ func BenchmarkReceiveFrom(b *testing.B) {
if _, err := sendConn.WriteTo(sendBuf, dstAddr); err != nil {
b.Fatalf("WriteTo: %v", err)
}
n, ep, addr, err := conn.ReceiveIPv4(buf)
n, ep, err := conn.ReceiveIPv4(buf)
if err != nil {
b.Fatal(err)
}
_ = n
_ = ep
_ = addr
}
}

View File

@@ -28,6 +28,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/net/packet"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
@@ -62,7 +63,46 @@ func Impl(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.
log.Fatal(err)
}
ipstack.AddAddress(nicID, ipv4.ProtocolNumber, tcpip.Address(net.ParseIP("100.96.188.101").To4()))
e.AddNetworkMapCallback(func(nm *controlclient.NetworkMap) {
oldIPs := make(map[tcpip.Address]bool)
for _, ip := range ipstack.AllAddresses()[nicID] {
oldIPs[ip.AddressWithPrefix.Address] = true
}
newIPs := make(map[tcpip.Address]bool)
for _, ip := range nm.Addresses {
newIPs[tcpip.Address(ip.IPNet().IP)] = true
}
ipsToBeAdded := make(map[tcpip.Address]bool)
for ip := range newIPs {
if !oldIPs[ip] {
ipsToBeAdded[ip] = true
}
}
ipsToBeRemoved := make(map[tcpip.Address]bool)
for ip := range oldIPs {
if !newIPs[ip] {
ipsToBeRemoved[ip] = true
}
}
for ip := range ipsToBeRemoved {
err := ipstack.RemoveAddress(nicID, ip)
if err != nil {
logf("netstack: could not deregister IP %s: %v", ip, err)
} else {
logf("netstack: deregistered IP %s", ip)
}
}
for ip := range ipsToBeAdded {
err := ipstack.AddAddress(nicID, ipv4.ProtocolNumber, ip)
if err != nil {
logf("netstack: could not register IP %s: %v", ip, err)
} else {
logf("netstack: registered IP %s", ip)
}
}
})
// Add 0.0.0.0/0 default route.
subnet, _ := tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 4)), tcpip.AddressMask(strings.Repeat("\x00", 4)))

View File

@@ -32,35 +32,47 @@ type pendingOpenFlow struct {
timer *time.Timer // until giving up on the flow
}
func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) {
e.mu.Lock()
defer e.mu.Unlock()
of, ok := e.pendOpen[f]
if !ok {
// Not a tracked flow (likely already removed)
return false
}
of.timer.Stop()
delete(e.pendOpen, f)
return true
}
func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) {
res = filter.Accept // always
if pp.IPProto == packet.TSMP {
res = filter.DropSilently
rh, ok := pp.AsTailscaleRejectedHeader()
if !ok {
return
}
if f := rh.Flow(); e.removeFlow(f) {
e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason)
}
return
}
if pp.IPVersion == 0 ||
pp.IPProto != packet.TCP ||
pp.TCPFlags&(packet.TCPSyn|packet.TCPRst) == 0 {
return
}
flow := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
// Either a SYN or a RST came back. Remove it in either case.
e.mu.Lock()
defer e.mu.Unlock()
of, ok := e.pendOpen[flow]
if !ok {
// Not a tracked flow.
return
f := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
removed := e.removeFlow(f)
if removed && pp.TCPFlags&packet.TCPRst != 0 {
e.logf("open-conn-track: flow TCP %v got RST by peer", f)
}
of.timer.Stop()
delete(e.pendOpen, flow)
if pp.TCPFlags&packet.TCPRst != 0 {
// TODO(bradfitz): have peer send a IP proto 99 "why"
// packet first with details and log that instead
// (e.g. ACL prohibited, shields up, etc).
e.logf("open-conn-track: flow %v got RST by peer", flow)
return
}
return
}

View File

@@ -237,7 +237,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid)
}
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
const mtu = 0
luid := winipcfg.LUID(tun.LUID())
iface, err := interfaceFromLUID(luid,
@@ -251,6 +251,15 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
return err
}
// Send non-nil return errors to retErrc, to interupt our background
// setPrivateNetwork goroutine.
retErrc := make(chan error, 1)
defer func() {
if retErr != nil {
retErrc <- retErr
}
}()
go func() {
// It takes a weirdly long time for Windows to notice the
// new interface has come up. Poll periodically until it
@@ -262,11 +271,18 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
log.Printf("setPrivateNetwork(try=%d): %v", i, err)
} else {
if found {
if i > 0 {
log.Printf("setPrivateNetwork(try=%d): success", i)
}
return
}
log.Printf("setPrivateNetwork(try=%d): not found", i)
}
time.Sleep(1 * time.Second)
select {
case <-time.After(time.Second):
case <-retErrc:
return
}
}
log.Printf("setPrivateNetwork: adapter LUID %v not found after %d tries, giving up", luid, tries)
}()

View File

@@ -366,7 +366,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
// address is already assigned to the interface, or if the addition
// fails.
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
if !r.v6Available && addr.IP.Is6() {
return nil
}
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
}
@@ -380,6 +382,9 @@ func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
// the address is not assigned to the interface, or if the removal
// fails.
func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error {
if !r.v6Available && addr.IP.Is6() {
return nil
}
if err := r.delLoopbackRule(addr.IP); err != nil {
return err
}
@@ -437,6 +442,9 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
// interface. Fails if the route already exists, or if adding the
// route fails.
func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
if !r.v6Available && cidr.IP.Is6() {
return nil
}
args := []string{
"ip", "route", "add",
normalizeCIDR(cidr),
@@ -452,6 +460,9 @@ func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
// interface. Fails if the route doesn't exist, or if removing the
// route fails.
func (r *linuxRouter) delRoute(cidr netaddr.IPPrefix) error {
if !r.v6Available && cidr.IP.Is6() {
return nil
}
args := []string{
"ip", "route", "del",
normalizeCIDR(cidr),

View File

@@ -201,12 +201,11 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
break
}
}
if !anyHasSuffix {
return netaddr.IP{}, dns.RCodeRefused, nil
}
addr, found := dnsMap.nameToIP[domain]
if !found {
if !anyHasSuffix {
return netaddr.IP{}, dns.RCodeRefused, nil
}
return netaddr.IP{}, dns.RCodeNameError, nil
}

View File

@@ -218,8 +218,8 @@ func (t *TUN) poll() {
func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
if t.PreFilterOut != nil {
if t.PreFilterOut(p, t) == filter.Drop {
return filter.Drop
if res := t.PreFilterOut(p, t); res.IsDrop() {
return res
}
}
@@ -234,8 +234,8 @@ func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
}
if t.PostFilterOut != nil {
if t.PostFilterOut(p, t) == filter.Drop {
return filter.Drop
if res := t.PostFilterOut(p, t); res.IsDrop() {
return res
}
}
@@ -264,12 +264,12 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) {
return 0, io.EOF
case err := <-t.errors:
return 0, err
case packet := <-t.outbound:
n = copy(buf[offset:], packet)
case pkt := <-t.outbound:
n = copy(buf[offset:], pkt)
// t.buffer has a fixed location in memory,
// so this is the easiest way to tell when it has been consumed.
// &packet[0] can be used because empty packets do not reach t.outbound.
if &packet[0] == &t.buffer[PacketStartOffset] {
// &pkt[0] can be used because empty packets do not reach t.outbound.
if &pkt[0] == &t.buffer[PacketStartOffset] {
t.bufferConsumed <- struct{}{}
} else {
// If the packet is not from t.buffer, then it is an injected packet.
@@ -307,8 +307,8 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
p.Decode(buf)
if t.PreFilterIn != nil {
if t.PreFilterIn(p, t) == filter.Drop {
return filter.Drop
if res := t.PreFilterIn(p, t); res.IsDrop() {
return res
}
}
@@ -319,22 +319,50 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
}
if filt.RunIn(p, t.filterFlags) != filter.Accept {
// Tell them, via TSMP, we're dropping them due to the ACL.
// Their host networking stack can translate this into ICMP
// or whatnot as required. But notably, their GUI or tailscale CLI
// can show them a rejection history with reasons.
if p.IPVersion == 4 && p.IPProto == packet.TCP && p.TCPFlags&packet.TCPSyn != 0 {
rj := packet.TailscaleRejectedHeader{
IPSrc: p.Dst.IP,
IPDst: p.Src.IP,
Src: p.Src,
Dst: p.Dst,
Proto: p.IPProto,
Reason: packet.RejectedDueToACLs,
}
if filt.ShieldsUp() {
rj.Reason = packet.RejectedDueToShieldsUp
}
pkt := packet.Generate(rj, nil)
t.InjectOutbound(pkt)
// TODO(bradfitz): also send a TCP RST, after the TSMP message.
}
return filter.Drop
}
if t.PostFilterIn != nil {
if t.PostFilterIn(p, t) == filter.Drop {
return filter.Drop
if res := t.PostFilterIn(p, t); res.IsDrop() {
return res
}
}
return filter.Accept
}
// Write accepts an incoming packet. The packet begins at buf[offset:],
// like wireguard-go/tun.Device.Write.
func (t *TUN) Write(buf []byte, offset int) (int, error) {
if !t.disableFilter {
response := t.filterIn(buf[offset:])
if response != filter.Accept {
res := t.filterIn(buf[offset:])
if res == filter.DropSilently {
return len(buf), nil
}
if res != filter.Accept {
return 0, ErrFiltered
}
}

View File

@@ -341,6 +341,22 @@ func TestAllocs(t *testing.T) {
}
}
func TestClose(t *testing.T) {
ftun, tun := newFakeTUN(t.Logf, false)
data := udp4("1.2.3.4", "5.6.7.8", 98, 98)
_, err := ftun.Write(data, 0)
if err != nil {
t.Error(err)
}
tun.Close()
_, err = ftun.Write(data, 0)
if err == nil {
t.Error("Expected error from ftun.Write() after Close()")
}
}
func BenchmarkWrite(b *testing.B) {
ftun, tun := newFakeTUN(b.Logf, true)
defer tun.Close()

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
@@ -47,6 +46,7 @@ import (
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/tsdns"
"tailscale.com/wgengine/tstun"
"tailscale.com/wgengine/wglog"
)
// minimalMTU is the MTU we set on tailscale's TUN
@@ -84,6 +84,7 @@ const (
type userspaceEngine struct {
logf logger.Logf
wgLogger *wglog.Logger //a wireguard-go logging wrapper
reqCh chan struct{}
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
timeNow func() time.Time
@@ -110,16 +111,18 @@ type userspaceEngine struct {
trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
sentActivityAt map[netaddr.IP]*int64 // value is atomic int64 of unixtime
destIPActivityFuncs map[netaddr.IP]func()
statusBufioReader *bufio.Reader // reusable for UAPI
mu sync.Mutex // guards following; see lock order comment below
closing bool // Close was called (even if we're still closing)
statusCallback StatusCallback
linkChangeCallback func(major bool, newState *interfaces.State)
peerSequence []wgkey.Key
endpoints []string
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
linkState *interfaces.State
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
mu sync.Mutex // guards following; see lock order comment below
closing bool // Close was called (even if we're still closing)
statusCallback StatusCallback
linkChangeCallback func(major bool, newState *interfaces.State)
peerSequence []wgkey.Key
endpoints []string
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
linkState *interfaces.State
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
networkMapCallbacks map[*someHandle]NetworkMapCallback
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
}
@@ -191,6 +194,7 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
e, err := NewUserspaceEngineAdvanced(conf)
if err != nil {
tun.Close()
return nil, err
}
return e, err
@@ -278,23 +282,9 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
e.tundev.PostFilterOut = e.trackOpenPostFilterOut
}
// wireguard-go logs as it starts and stops routines.
// Silence those; there are a lot of them, and they're just noise.
allowLogf := func(s string) bool {
return !strings.HasPrefix(s, "Routine:")
}
filtered := logger.Filtered(logf, allowLogf)
// flags==0 because logf is already nested in another logger.
// The outer one can display the preferred log prefixes, etc.
dlog := logger.StdLogger(filtered)
logger := device.Logger{
Debug: dlog,
Info: dlog,
Error: dlog,
}
e.wgLogger = wglog.NewLogger(logf)
opts := &device.DeviceOptions{
Logger: &logger,
Logger: e.wgLogger.DeviceLogger,
HandshakeDone: func(peerKey device.NoisePublicKey, peer *device.Peer, deviceAllowedIPs *device.AllowedIPs) {
// Send an unsolicited status event every time a
// handshake completes. This makes sure our UI can
@@ -773,6 +763,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
}
full := e.lastCfgFull
e.wgLogger.SetPeers(full.Peers)
// Compute a minimal config to pass to wireguard-go
// based on the full config. Prune off all the peers
@@ -806,11 +797,14 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
}
continue
}
tsIP := p.AllowedIPs[0].IP
dk := discoKeyFromPeer(p)
trackDisco = append(trackDisco, dk)
trackIPs = append(trackIPs, tsIP)
if e.isActiveSince(dk, tsIP, activeCutoff) {
recentlyActive := false
for _, cidr := range p.AllowedIPs {
trackIPs = append(trackIPs, cidr.IP)
recentlyActive = recentlyActive || e.isActiveSince(dk, cidr.IP, activeCutoff)
}
if recentlyActive {
min.Peers = append(min.Peers, *p)
if discoChanged[key.Public(p.PublicKey)] {
needRemoveStep = true
@@ -1042,8 +1036,8 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
return e.statusCallback
}
// TODO: this function returns an error but it's always nil, and when
// there's actually a problem it just calls log.Fatal. Why?
var singleNewline = []byte{'\n'}
func (e *userspaceEngine) getStatus() (*Status, error) {
// Grab derpConns before acquiring wgLock to not violate lock ordering;
// the DERPs method acquires magicsock.Conn.mu.
@@ -1068,15 +1062,12 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
return nil, nil
}
// lineLen is the max UAPI line we expect. The longest I see is
// len("preshared_key=")+64 hex+"\n" == 79. Add some slop.
const lineLen = 100
pr, pw := io.Pipe()
defer pr.Close() // to unblock writes on error path returns
errc := make(chan error, 1)
go func() {
defer pw.Close()
bw := bufio.NewWriterSize(pw, lineLen)
// TODO(apenwarr): get rid of silly uapi stuff for in-process comms
// FIXME: get notified of status changes instead of polling.
filter := device.IPCGetFilter{
@@ -1084,23 +1075,34 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
// unused below; request that they not be sent instead.
FilterAllowedIPs: true,
}
if err := e.wgdev.IpcGetOperationFiltered(bw, filter); err != nil {
errc <- fmt.Errorf("IpcGetOperation: %w", err)
return
err := e.wgdev.IpcGetOperationFiltered(pw, filter)
if err != nil {
err = fmt.Errorf("IpcGetOperation: %w", err)
}
errc <- bw.Flush()
errc <- err
}()
pp := make(map[wgkey.Key]*PeerStatus)
p := &PeerStatus{}
var hst1, hst2, n int64
var err error
bs := bufio.NewScanner(pr)
bs.Buffer(make([]byte, lineLen), lineLen)
for bs.Scan() {
line := bs.Bytes()
br := e.statusBufioReader
if br != nil {
br.Reset(pr)
} else {
br = bufio.NewReaderSize(pr, 1<<10)
e.statusBufioReader = br
}
for {
line, err := br.ReadSlice('\n')
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("reading from UAPI pipe: %w", err)
}
line = bytes.TrimSuffix(line, singleNewline)
k := line
var v mem.RO
if i := bytes.IndexByte(line, '='); i != -1 {
@@ -1111,7 +1113,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
case "public_key":
pk, err := key.NewPublicFromHexMem(v)
if err != nil {
log.Fatalf("IpcGetOperation: invalid key %#v", v)
return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line)
}
p = &PeerStatus{}
pp[wgkey.Key(pk)] = p
@@ -1122,34 +1124,31 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
n, err = mem.ParseInt(v, 10, 64)
p.RxBytes = ByteCount(n)
if err != nil {
log.Fatalf("IpcGetOperation: rx_bytes invalid: %#v", line)
return nil, fmt.Errorf("IpcGetOperation: rx_bytes invalid: %#v", line)
}
case "tx_bytes":
n, err = mem.ParseInt(v, 10, 64)
p.TxBytes = ByteCount(n)
if err != nil {
log.Fatalf("IpcGetOperation: tx_bytes invalid: %#v", line)
return nil, fmt.Errorf("IpcGetOperation: tx_bytes invalid: %#v", line)
}
case "last_handshake_time_sec":
hst1, err = mem.ParseInt(v, 10, 64)
if err != nil {
log.Fatalf("IpcGetOperation: hst1 invalid: %#v", line)
return nil, fmt.Errorf("IpcGetOperation: hst1 invalid: %#v", line)
}
case "last_handshake_time_nsec":
hst2, err = mem.ParseInt(v, 10, 64)
if err != nil {
log.Fatalf("IpcGetOperation: hst2 invalid: %#v", line)
return nil, fmt.Errorf("IpcGetOperation: hst2 invalid: %#v", line)
}
if hst1 != 0 || hst2 != 0 {
p.LastHandshake = time.Unix(hst1, hst2)
} // else leave at time.IsZero()
}
}
if err := bs.Err(); err != nil {
log.Fatalf("reading IpcGetOperation output: %v", err)
}
if err := <-errc; err != nil {
log.Fatalf("IpcGetOperation: %v", err)
return nil, fmt.Errorf("IpcGetOperation: %v", err)
}
e.mu.Lock()
@@ -1290,6 +1289,21 @@ func (e *userspaceEngine) SetLinkChangeCallback(cb func(major bool, newState *in
}
}
func (e *userspaceEngine) AddNetworkMapCallback(cb NetworkMapCallback) func() {
e.mu.Lock()
defer e.mu.Unlock()
if e.networkMapCallbacks == nil {
e.networkMapCallbacks = make(map[*someHandle]NetworkMapCallback)
}
h := new(someHandle)
e.networkMapCallbacks[h] = cb
return func() {
e.mu.Lock()
defer e.mu.Unlock()
delete(e.networkMapCallbacks, h)
}
}
func getLinkState() (*interfaces.State, error) {
s, err := interfaces.GetState()
if s != nil {
@@ -1308,6 +1322,15 @@ func (e *userspaceEngine) SetDERPMap(dm *tailcfg.DERPMap) {
func (e *userspaceEngine) SetNetworkMap(nm *controlclient.NetworkMap) {
e.magicConn.SetNetworkMap(nm)
e.mu.Lock()
callbacks := make([]NetworkMapCallback, 0, 4)
for _, fn := range e.networkMapCallbacks {
callbacks = append(callbacks, fn)
}
e.mu.Unlock()
for _, fn := range callbacks {
fn(nm)
}
}
func (e *userspaceEngine) DiscoPublicKey() tailcfg.DiscoKey {

View File

@@ -110,6 +110,11 @@ func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) {
func (e *watchdogEngine) SetNetworkMap(nm *controlclient.NetworkMap) {
e.watchdog("SetNetworkMap", func() { e.wrap.SetNetworkMap(nm) })
}
func (e *watchdogEngine) AddNetworkMapCallback(callback NetworkMapCallback) func() {
var fn func()
e.watchdog("AddNetworkMapCallback", func() { fn = e.wrap.AddNetworkMapCallback(callback) })
return func() { e.watchdog("RemoveNetworkMapCallback", fn) }
}
func (e *watchdogEngine) DiscoPublicKey() (k tailcfg.DiscoKey) {
e.watchdog("DiscoPublicKey", func() { k = e.wrap.DiscoPublicKey() })
return k

View File

@@ -49,6 +49,15 @@ type StatusCallback func(*Status, error)
// NetInfoCallback is the type used by Engine.SetNetInfoCallback.
type NetInfoCallback func(*tailcfg.NetInfo)
// NetworkMapCallback is the type used by callbacks that hook
// into network map updates.
type NetworkMapCallback func(*controlclient.NetworkMap)
// someHandle is allocated so its pointer address acts as a unique
// map key handle. (It needs to have non-zero size for Go to guarantee
// the pointer is unique.)
type someHandle struct{ _ byte }
// ErrNoChanges is returned by Engine.Reconfig if no changes were made.
var ErrNoChanges = errors.New("no changes made to Engine config")
@@ -114,6 +123,12 @@ type Engine interface {
// The network map should only be read from.
SetNetworkMap(*controlclient.NetworkMap)
// AddNetworkMapCallback adds a function to a list of callbacks
// that are called when the network map updates. It returns a
// function that when called would remove the function from the
// list of callbacks.
AddNetworkMapCallback(NetworkMapCallback) (removeCallback func())
// SetNetInfoCallback sets the function to call when a
// new NetInfo summary is available.
SetNetInfoCallback(NetInfoCallback)

89
wgengine/wglog/wglog.go Normal file
View File

@@ -0,0 +1,89 @@
// 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 wglog contains logging helpers for wireguard-go.
package wglog
import (
"encoding/base64"
"fmt"
"strings"
"sync/atomic"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/types/logger"
)
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
// It can be modified at run time to adjust to new wireguard-go configurations.
type Logger struct {
DeviceLogger *device.Logger
replacer atomic.Value // of *strings.Replacer
}
// NewLogger creates a new logger for use with wireguard-go.
// This logger silences repetitive/unhelpful noisy log lines
// and rewrites peer keys from wireguard-go into Tailscale format.
func NewLogger(logf logger.Logf) *Logger {
ret := new(Logger)
wrapper := func(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if strings.Contains(msg, "Routine:") {
// wireguard-go logs as it starts and stops routines.
// Drop those; there are a lot of them, and they're just noise.
return
}
r := ret.replacer.Load()
if r == nil {
// No replacements specified; log as originally planned.
logf(format, args...)
return
}
// Do the replacements.
new := r.(*strings.Replacer).Replace(msg)
if new == msg {
// No replacements. Log as originally planned.
logf(format, args...)
return
}
// We made some replacements. Log the new version.
// This changes the format string,
// which is somewhat unfortunate as it impacts rate limiting,
// but there's not much we can do about that.
logf("%s", new)
}
std := logger.StdLogger(wrapper)
ret.DeviceLogger = &device.Logger{
Debug: std,
Info: std,
Error: std,
}
return ret
}
// SetPeers adjusts x to rewrite the peer public keys found in peers.
// SetPeers is safe for concurrent use.
func (x *Logger) SetPeers(peers []wgcfg.Peer) {
// Construct a new peer public key log rewriter.
var replace []string
for _, peer := range peers {
old := "peer(" + wireguardGoString(peer.PublicKey) + ")"
new := peer.PublicKey.ShortString()
replace = append(replace, old, new)
}
r := strings.NewReplacer(replace...)
x.replacer.Store(r)
}
// wireguardGoString prints p in the same format used by wireguard-go.
func wireguardGoString(k wgcfg.Key) string {
base64Key := base64.StdEncoding.EncodeToString(k[:])
abbreviatedKey := "invalid"
if len(base64Key) == 44 {
abbreviatedKey = base64Key[0:4] + "…" + base64Key[39:43]
}
return abbreviatedKey
}

View File

@@ -0,0 +1,59 @@
// 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 wglog_test
import (
"fmt"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/wgengine/wglog"
)
func TestLogger(t *testing.T) {
tests := []struct {
in string
want string
omit bool
}{
{"hi", "hi", false},
{"Routine: starting", "", true},
{"peer(IMTB…r7lM) says it misses you", "[IMTBr] says it misses you", false},
}
c := make(chan string, 1)
logf := func(format string, args ...interface{}) {
s := fmt.Sprintf(format, args...)
select {
case c <- s:
default:
t.Errorf("wrote %q, but shouldn't have", s)
}
}
x := wglog.NewLogger(logf)
key, err := wgcfg.ParseHexKey("20c4c1ae54e1fd37cab6e9a532ca20646aff496796cc41d4519560e5e82bee53")
if err != nil {
t.Fatal(err)
}
x.SetPeers([]wgcfg.Peer{{PublicKey: key}})
for _, tt := range tests {
if tt.omit {
// Write a message ourselves into the channel.
// Then if logf also attempts to write into the channel, it'll fail.
c <- ""
}
x.DeviceLogger.Info.Println(tt.in)
got := <-c
if tt.omit {
continue
}
tt.want += "\n"
if got != tt.want {
t.Errorf("Println(%q) = %q want %q", tt.in, got, tt.want)
}
}
}