Compare commits

...

98 Commits

Author SHA1 Message Date
David Anderson
9c0a1375eb WIP: kernel accelerated tailscaled 2021-08-30 14:30:06 -07:00
Will Lachance
a35c3ba221 cmd/tailscale: fix truncated characters in web controller (#2722)
Fixes #2204

Signed-off-by: William Lachance <wlach@protonmail.com>

Co-authored-by: William Lachance <wlach@protonmail.com>
Co-authored-by: Ross Zurowski <ross@rosszurowski.com>
2021-08-27 17:10:17 -04:00
Brad Fitzpatrick
83906abc5e wgengine/netstack: clarify a comment
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-27 11:10:56 -07:00
Maisem Ali
ae9b3f38d6 github: set GOOS/GOARCH for go list
Currently we do not set the env variables for `go list ./...` resulting
in errors like
```
build constraints exclude all Go files in
/home/runner/work/tailscale/tailscale/chirp
```

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-08-26 16:27:37 -07:00
Maisem Ali
baf8854f9a tempfork/wireguard-windows: remove the old windows firewall code now that we are no
longer relying on it.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-08-26 15:00:45 -07:00
Brad Fitzpatrick
3606e68721 net/interfaces: fix default route lookup on Windows
It wasn't using the right metric. Apparently you're supposed to sum the route
metric and interface metric. Whoops.

While here, optimize a few little things too, not that this code
should be too hot.

Fixes #2707 (at least; probably dups but I'm failing to find)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-26 13:42:20 -07:00
Brad Fitzpatrick
4aab083cae cmd/tailscaled: add debug flag to print interfaces just once
It previously only had a polling monitor mode.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-26 11:59:52 -07:00
David Anderson
b49d9bc74d net/portmapper: fix "running a test" condition.
Fixes #2686.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-08-25 20:16:35 -07:00
Brad Fitzpatrick
1925fb584e wgengine/netstack: fix crash in userspace netstack TCP forwarding
Fixes #2658

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-25 15:48:05 -07:00
Brad Fitzpatrick
88bd796622 tailcfg,ipn/ipnlocal: support DNSConfig.Routes with empty values [mapver 23]
Fixes #2706
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-25 11:35:48 -07:00
slowy07
ac0353e982 fix: typo spelling grammar
Signed-off-by: slowy07 <slowy.arfy@gmail.com>
2021-08-24 07:55:04 -07:00
Denton Gentry
780e65a613 VERSION.txt: new unstable v1.15.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-23 13:38:15 -07:00
Brad Fitzpatrick
37053801bb wgengine/magicsock: restore a bit of logging on node becoming active
Fixes #2695

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-23 12:22:23 -07:00
Brad Fitzpatrick
51976ab3a2 tsweb: add vars for unix process start time and version
To be scraped in the Go expvar JSON format, as a string is involved.

For a future tool to record when processes restarted exactly, and at
what version.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-23 10:36:28 -07:00
Brad Fitzpatrick
246fa67e56 version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-23 09:16:45 -07:00
Brad Fitzpatrick
6990a314f5 hostinfo: set DeviceModel from Linux devicetree model
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-22 21:28:36 -07:00
Brad Fitzpatrick
3ac731dda1 hostinfo: fix earlier git fail, add files lost in move
This was meant to be part of 47045265b9
which instead deleted them :(

Updates tailscale/corp#1959
2021-08-22 21:14:04 -07:00
Aaditya Chaudhary
71b375c502 api.md: add acl validation docs
Signed-off-by: Aaditya Chaudhary <32117362+AadityaChaudhary@users.noreply.github.com>
2021-08-22 18:27:45 -04:00
David Crawshaw
0ac2130590 net/dns: resolveConfExists reading the wrong error
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-08-21 20:15:51 -07:00
Matt Drollette
c1aa5a2e33 ipn/ipnlocal: update requested tags in host info
Fixes #2641

Signed-off-by: Matt Drollette <matt@drollette.com>
2021-08-21 20:07:00 -07:00
Brad Fitzpatrick
f35b8c3ead derp: fix meshing accounting edge case bug
If a peer is connected to multiple nodes in a region (so
multiForwarder is in use) and then a node restarts and re-sends all
its additions, this bug about whether an element is in the
multiForwarder could cause a one-time flip in the which peer node we
forward to.  Note a huge deal, but not written as intended.

Thanks to @lewgun for the bug report in #2141.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-21 19:54:55 -07:00
Alessandro Mingione
fab296536c words: add more tails and scales
Signed-off-by: Alessandro Mingione <alessandro@tailscale.com>
2021-08-21 16:35:00 -07:00
Denton Gentry
6731f934a6 Revert "wgengine: actively log FlushDNS."
This log is quite verbose, it was only to be left in for one
unstable build to help debug a user issue.

This reverts commit 1dd2552032.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-20 18:12:47 -07:00
Brad Fitzpatrick
47045265b9 hostinfo: add SetDeviceModel setter, move remaining code from controlclient
Updates tailscale/corp#1959

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-20 10:45:22 -07:00
Josh Bleecher Snyder
4ff0757d44 cmd/testcontrol: add test control server
This is useful for manual performance testing
of networks with many nodes.
I imagine it'll grow more knobs over time.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-19 17:50:48 -07:00
Denton Gentry
1dd2552032 wgengine: actively log FlushDNS.
Intended to help in resolving customer issue with
DNS caching.

We currently exec `ipconfig /flushdns` from two
places:
- SetDNS(), which logs before invoking
- here in router_windows, which doesn't

We'd like to see a positive indication in logs that flushdns
is being run.

As this log is expected to be spammy, it is proposed to
leave this in just long enough to do an unstable 1.13.x build
and then revert it. They won't run an unsigned image that
I build.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-19 14:43:14 -07:00
Brad Fitzpatrick
36ffd509de net/dns: avoid Linux PolicyKit GUI dialog during tests
Fixes #2672

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-19 08:58:47 -07:00
Brad Fitzpatrick
edb338f542 cmd/tailscale: fix sporadic 'context canceled' error on 'up'
Fixes #2333

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-19 08:44:39 -07:00
Brad Fitzpatrick
faa891c1f2 client/tailscale,ipn/localapi: warn on tailscale/tailscaled version skew
Fixes #1905

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-19 08:36:13 -07:00
Brad Fitzpatrick
8269a23758 version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-19 08:28:08 -07:00
David Anderson
bf8556ab86 portlist: fix build tag to build only on macOS, not macOS+iOS. 2021-08-18 16:18:02 -07:00
Josh Bleecher Snyder
6ef734e493 wgengine: predict min.Peers length across calls
The number of peers we have will be pretty stable across time.
Allocate roughly the right slice size.
This reduces memory usage when there are many peers.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-18 16:12:45 -07:00
Josh Bleecher Snyder
adf696172d wgengine/userspace: reduce allocations in getStatus
Two optimizations.

Use values instead of pointers.
We were using pointers to make track the "peer in progress" easier.
It's not too hard to do it manually, though.

Make two passes through the data, so that we can size our
return value accurately from the beginning.
This is cheap enough compared to the allocation,
which grows linearly in the number of peers,
that it is worth doing.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-18 16:12:08 -07:00
Brad Fitzpatrick
af30897f0d Makefile: add a linux/arm check
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 15:36:09 -07:00
Maisem Ali
1f006025c2 net/tstun: fix build on arm
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-08-18 15:28:14 -07:00
Josh Bleecher Snyder
fcca374fa7 tstest/integration/testcontrol: sort peers in map response
This is part of the control protocol.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-18 14:49:26 -07:00
Brad Fitzpatrick
cd426eaf4c net/portmapper: fix t.Log-after-test-done race in tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 14:39:45 -07:00
Maisem Ali
9f62cc665e tailscaled: try migrating old state on synology devices
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-08-18 13:45:15 -07:00
Maisem Ali
5c383bdf5d wgengine/router: pass in AmbientCaps when calling ip rule
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-08-18 13:28:53 -07:00
Brad Fitzpatrick
56db3e2548 ipn/localapi: refresh ACME certs in background two weeks in advance
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 12:55:29 -07:00
Brad Fitzpatrick
6f8c8c771b control/controlclient: tweak a couple error messages
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 10:11:38 -07:00
Brad Fitzpatrick
b7ae529ecc client/tailscale: make GetCertificate guess cert if SNI lacks dots
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 10:08:44 -07:00
Simeng He
e199e407d2 tailcfg: add IP and Types field to PingRequest
Signed-off-by: Simeng He <simeng@tailscale.com>
2021-08-18 12:23:24 -04:00
Brad Fitzpatrick
d5e1abd0c4 cmd/tailscale/cli: only write cert file if it changed
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-18 08:19:44 -07:00
Brad Fitzpatrick
57b794c338 ipn/localapi: move cert fetching code to localapi, cache, add cert subcommand
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-17 16:02:10 -07:00
Josh Bleecher Snyder
4c8b5fdec4 control/controlclient: do not periodically print full netmap
The netmaps can get really large.
Printing, processing, and uploading them is expensive.
Only print the header on an ongoing basis.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 12:57:49 -07:00
Josh Bleecher Snyder
a666b546fb ipn/ipnlocal: log number of packet filters rather than entire filter
The number of packet filters can grow very large,
so this log entry can be very large.
We can get the packet filter server-side,
so reduce verbosity here to just the number of filters present.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 12:57:49 -07:00
Josh Bleecher Snyder
0c038b477f logtail: add a re-usable buffer for uploads
This avoids a per-upload alloc (which in practice
often means per-log-line), up to 4k.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 12:56:57 -07:00
Josh Bleecher Snyder
278e7de9c9 logtail: always send a json array
The code goes to some effort to send a single JSON object
when there's only a single line and a JSON array when there
are multiple lines.

It makes the code more complex and more expensive;
when we add a second line, we have to use a second buffer
to duplicate the first one after adding a leading square brackets.

The savings come to two bytes. Instead, always send an array.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 12:56:57 -07:00
Josh Bleecher Snyder
93284209bc logtail/filch: preallocate a scanner buffer
Scanning log lines is a frequent source of allocations.
Pre-allocate a re-usable buffer.

This still doesn't help when there are giant log lines.
Those will still be problematic from an iOS memory perspective.
For more on that, see https://github.com/tailscale/corp/issues/2423.

(For those who cannot follow that link, it is a discussion
of particular problematic types of log lines for
particular categories of customers. The "categories of customers"
part is the reason that it is a private issue.)

There is also a latent bug here. If we ever encounter
a log line longer than bufio.MaxScanTokenSize,
then bufio.Scan will return an error,
and we'll truncate the file and discard the rest of the log.
That's not good, but bufio.MaxScanTokenSize is really big,
so it probably doesn't matter much in practice now.
Unfortunately, it does prevent us from easily capping the potential
memory usage here, on pain of losing log entries.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 12:32:10 -07:00
Matt Layher
8ab44b339e net/tstun: use unix.Ifreq type for Linux TAP interface configuration
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2021-08-17 12:17:51 -07:00
Josh Bleecher Snyder
6da6d47a83 all: simplify build tags involving iOS
Prior to Go 1.16, iOS used GOOS=darwin,
so we had to distinguish macOS from iOS during GOARCH.

We now require Go 1.16 in our go.mod, so we can simplify.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 11:13:03 -07:00
Josh Bleecher Snyder
a24cee0d67 all: simplify ts_macext build tags
Now that we have the easier-to-parse go:build build tags,
it is straightforward to simplify them. Yay.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 11:13:03 -07:00
Josh Bleecher Snyder
d2aa144dcc syncs: bump known good version to include Go 1.17
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-17 11:13:03 -07:00
Brad Fitzpatrick
25e060a841 cmd/tailscale/cli: fix cert fetch WaitOrder retry loop, misc cleanups
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-16 14:54:41 -07:00
Brad Fitzpatrick
833200da6f net/tstun: don't exec uname -r on Linux in TUN failure diagnostics
Fixes https://twitter.com/zekjur/status/1425557520513486848

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-16 12:18:40 -07:00
Brad Fitzpatrick
e804ab29fd net/tstun: move TUN failure diagnostics to OS-specific files
Mostly so the Linux one can use Linux-specific stuff in package
syscall and not use os/exec for uname for portability.

But also it helps deps a tiny bit on iOS.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-16 11:24:25 -07:00
Brad Fitzpatrick
b2eea1ee00 cmd/tailscale/cli: make cert fetch registration automatic, show valid domains
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-16 10:45:05 -07:00
Brad Fitzpatrick
39610aeb09 wgengine/magicsock: move debug knobs to their own file, compile out on iOS
No need for these knobs on iOS where you can set the environment
variables anyway.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-15 13:21:22 -07:00
Denton Gentry
98d557dd24 Dockerfile: use alpine:3.14
golang:1.16-alpine has updated to Alpine 3.14,
update the system image to match.

Reported by @kubeworm
https://twitter.com/kubeworm/status/1426751941519020033

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-14 22:46:34 -07:00
Denton Gentry
3e7ff5ff98 cmd/tailscaled: enable hybrid netstack mode for FreeBSD.
Allows FreeBSD to function as an exit node in the same way
that Windows and Tailscaled-on-MacOS do.

RELNOTE=FreeBSD can now function as an exit node.

Fixes https://github.com/tailscale/tailscale/issues/2498

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-14 20:26:38 -07:00
Brad Fitzpatrick
954867fef5 words: fix parser to handle missing newline at end
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 19:37:47 -07:00
Brad Fitzpatrick
c992504375 words: group some scales, support comments
Not sure how we missed mixolydian.

Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 15:46:52 -07:00
Brad Fitzpatrick
1bca722824 words: add five types of tales, best dog
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 15:32:13 -07:00
Brad Fitzpatrick
00b4c2331b words: add accessors and tests for a few of our favorite words
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 15:30:10 -07:00
Charlotte Brandhorst-Satzkorn
9547669787 words: these are a few more of my favorite words
Updates #1235

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@catzkorn.dev>
2021-08-13 16:26:36 -04:00
Charlotte Brandhorst-Satzkorn
b5a41ff381 words: these are a few of my favorite words
Updates #1235

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@catzkorn.dev>
2021-08-13 15:58:59 -04:00
Brad Fitzpatrick
ec9f3f4cc0 cmd/tailscale: update depaware
Missing from prior commit.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 08:56:29 -07:00
Brad Fitzpatrick
c68a12afe9 cmd/tailscale: add temporary debug command for getting DNS-01 LetsEncrypt cert
Not even close to usable or well integrated yet, but submitting this before
it bitrots or I lose it.

Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-13 08:49:49 -07:00
David Anderson
d2d55bd63c cmd/microproxy: delete.
Thank goodness.

Fixes #2635

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-08-12 14:17:42 -07:00
Brad Fitzpatrick
c6740da624 tsweb: make VarzHandler support untyped expvar.Maps for compatibility
Updates #2635

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-12 13:35:31 -07:00
Brad Fitzpatrick
7c7eb8094b tsweb: make VarzHandler support expvar.Funcs returning ints/floats
Updates #2635
2021-08-12 13:07:34 -07:00
Brad Fitzpatrick
5aba620fb9 tsweb: make VarzHandler capable of walking structs with reflect
To be used by control, per linked bug's plan.

Updates #2635

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-11 18:31:54 -07:00
julianknodt
b9bd7dbc5d net/portmapper: log upnp information
This logs some basic statistics for UPnP, so that tailscale can better understand what routers
are being used and how to connect to them.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-10 22:45:00 -07:00
julianknodt
26b6fe7f02 net/portmapper: add PCP integration test
This adds a PCP test to the IGD test server, by hardcoding in a few observed packets from
Denton's box.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-10 15:14:46 -07:00
Brad Fitzpatrick
3700cf9ea4 tsweb: also support LabelMaps from expvar.Map, without metrics
We want to use tsweb to format Prometheus-style metrics from
our temporary golang.org/x/net/http2 fork, but we don't want http2
to depend on the tailscale.com module to use the concrete type
tailscale.com/metrics.LabelMap. Instead, let a expvar.Map be used
instead of it's annotated sufficiently in its name.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-10 14:31:54 -07:00
Brad Fitzpatrick
5f45d8f8e6 tsweb: add VarzHandler tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-10 13:41:16 -07:00
Josh Bleecher Snyder
a4e19f2233 version: remove rsc.io/goversion dependency
rsc.io/goversion is really expensive.
Running version.ReadExe on tailscaled on darwin
allocates 47k objects, almost 11mb.

All we want is the module info. For that, all we need to do
is scan through the binary looking for the magic start/end strings
and then grab the bytes in between them.

We can do that easily and quickly with nothing but a 64k buffer.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-09 22:46:01 -07:00
Brad Fitzpatrick
bdb93c5942 net/portmapper: actually test something in TestProbeIntegration
And use dynamic port numbers in tests, as Linux on GitHub Actions and
Windows in general have things running on these ports.

Co-Author: Julian Knodt <julianknodt@gmail.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-09 19:49:02 -07:00
Denton Gentry
26c1183941 hostinfo: add fly.io detection
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-09 09:54:24 -07:00
Denton Gentry
0796c53404 tsnet: add AuthKey support.
Set a TS_AUTHKEY environment variable to "tskey-01234..."

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-09 09:30:09 -07:00
Adrian Dewhurst
8bdf878832 net/dns/resolver: use forwarded dns txid directly
Previously, we hashed the question and combined it with the original
txid which was useful when concurrent queries were multiplexed on a
single local source port. We encountered some situations where the DNS
server canonicalizes the question in the response (uppercase converted
to lowercase in this case), which resulted in responses that we couldn't
match to the original request due to hash mismatches. This includes a
new test to cover that situation.

Fixes #2597

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-08-06 14:56:11 -04:00
David Crawshaw
360223fccb types/dnstype: introduce new package for Resolver
So the type can be used in net/dns without introducing a tailcfg
dependency.

For #2596

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

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

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

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

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

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

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

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

Updates #2589

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

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

Fixes #2109

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

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

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

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

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

Removing unnecessary details speeds up hashing:

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

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-08-03 20:35:57 -07:00
187 changed files with 5020 additions and 3732 deletions

View File

@@ -31,16 +31,28 @@ jobs:
run: "staticcheck -version"
- name: Run staticcheck (linux/amd64)
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
env:
GOOS: linux
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (darwin/amd64)
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
env:
GOOS: darwin
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/amd64)
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
env:
GOOS: windows
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/386)
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
env:
GOOS: windows
GOARCH: "386"
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -61,6 +61,6 @@ RUN go install -tags=xversion -ldflags="\
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/...
FROM alpine:3.11
FROM alpine:3.14
RUN apk add --no-cache ca-certificates iptables iproute2
COPY --from=build-env /go/bin/* /usr/local/bin/

View File

@@ -18,7 +18,10 @@ buildwindows:
build386:
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
check: staticcheck vet depaware buildwindows build386
buildlinuxarm:
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)

View File

@@ -1 +1 @@
1.13.0
1.15.0

47
api.md
View File

@@ -18,6 +18,7 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
- [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
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [DNS](#tailnet-dns)
@@ -473,7 +474,7 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
###### Query Parameters
`type` - can be 'user' or 'ipport'
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
The provided ACL is queried with this paramater to determine which rules match.
The provided ACL is queried with this parameter to determine which rules match.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
@@ -510,6 +511,50 @@ Response:
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```
<a name=tailnet-acl-validate-post></a>
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
##### Parameters
###### POST Body
The POST body should be a JSON formatted array of ACL Tests.
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
##### Example
```
POST /api/v2/tailnet/example.com/acl/validate
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
-u "tskey-yourapikey123:" \
--data-binary '
{
[
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
]
}'
```
Response:
If all the tests pass, the response will be empty, with an http status code of 200.
Failed test error response:
A 400 http status code and the errors in the response body.
```
{
"message":"test(s) failed",
"data":[
{
"user":"user1@example.com",
"errors":["address \"2.2.2.2:22\": want: Drop, got: Accept"]
}
]
}
```
<a name=tailnet-devices></a>
### Devices

View File

@@ -8,6 +8,7 @@ package tailscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -16,15 +17,19 @@ import (
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
// TailscaledSocket is the tailscaled Unix socket.
@@ -91,6 +96,9 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
return nil, err
}
defer res.Body.Close()
if server := res.Header.Get("Tailscale-Version"); server != version.Long {
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", version.Long, server)
}
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
@@ -293,3 +301,69 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
}
return &derpMap, nil
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}

View File

@@ -1,173 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// microproxy proxies incoming HTTPS connections to another
// destination. Instead of managing its own TLS certificates, it
// borrows issued certificates and keys from an autocert directory.
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"path/filepath"
"strings"
"sync"
"time"
"tailscale.com/logpolicy"
"tailscale.com/tsweb"
)
var (
addr = flag.String("addr", ":4430", "server address")
certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
hostname = flag.String("hostname", "", "hostname to serve")
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
insecure = flag.Bool("insecure", false, "serve over http, for development")
)
func main() {
flag.Parse()
if *logCollection != "" {
logpolicy.New(*logCollection)
}
ne, err := url.Parse(*nodeExporter)
if err != nil {
log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
}
proxy := httputil.NewSingleHostReverseProxy(ne)
proxy.FlushInterval = time.Second
if _, err = url.Parse(*goVarsURL); err != nil {
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
}
mux := http.NewServeMux()
tsweb.Debugger(mux) // registers /debug/*
mux.Handle("/metrics", tsweb.Protected(proxy))
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
Quiet200s: true,
Logf: log.Printf,
})))
ch := &certHolder{
hostname: *hostname,
path: filepath.Join(*certdir, *hostname),
}
httpsrv := &http.Server{
Addr: *addr,
Handler: mux,
}
if !*insecure {
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
err = httpsrv.ListenAndServeTLS("", "")
} else {
err = httpsrv.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
type goVarsHandler struct {
url string
}
func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
for k, i := range obj {
if prefix != "" {
k = prefix + "_" + k
}
switch v := i.(type) {
case map[string]interface{}:
promPrint(w, k, v)
case float64:
const saveConfigReject = "control_save_config_rejected_"
const saveConfig = "control_save_config_"
switch {
case strings.HasPrefix(k, saveConfigReject):
fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
case strings.HasPrefix(k, saveConfig):
fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
default:
fmt.Fprintf(w, "%s %f\n", k, v)
}
default:
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
}
}
}
func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
resp, err := http.Get(h.url)
if err != nil {
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
}
defer resp.Body.Close()
var mon map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
}
w.WriteHeader(http.StatusOK)
promPrint(w, "", mon)
return nil
}
// certHolder loads and caches a TLS certificate from disk, reloading
// it every hour.
type certHolder struct {
hostname string // only hostname allowed in SNI
path string // path of certificate+key combined PEM file
mu sync.Mutex
cert *tls.Certificate // cached parsed cert+key
loaded time.Time
}
func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
if ch.ServerName != c.hostname {
return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
}
c.mu.Lock()
defer c.mu.Unlock()
if time.Since(c.loaded) > time.Hour {
if err := c.loadLocked(); err != nil {
log.Printf("Reloading cert %q: %v", c.path, err)
// continue anyway, we might be able to serve off the stale cert.
}
}
return c.cert, nil
}
// load reloads the TLS certificate and key from disk. Caller must
// hold mu.
func (c *certHolder) loadLocked() error {
bs, err := ioutil.ReadFile(c.path)
if err != nil {
return fmt.Errorf("reading %q: %v", c.path, err)
}
cert, err := tls.X509KeyPair(bs, bs)
if err != nil {
return fmt.Errorf("parsing %q: %v", c.path, err)
}
c.cert = &cert
c.loaded = time.Now()
return nil
}

107
cmd/tailscale/cli/cert.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"crypto/tls"
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/atomicfile"
"tailscale.com/client/tailscale"
)
var certCmd = &ffcli.Command{
Name: "cert",
Exec: runCert,
ShortHelp: "get TLS certs",
ShortUsage: "cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("cert", flag.ExitOnError)
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file; defaults to DOMAIN.crt")
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file; defaults to DOMAIN.key")
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
return fs
})(),
}
var certArgs struct {
certFile string
keyFile string
serve bool
}
func runCert(ctx context.Context, args []string) error {
if certArgs.serve {
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: tailscale.GetCertificate,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
return
}
}
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
}),
}
log.Printf("running TLS server on :443 ...")
return s.ListenAndServeTLS("", "")
}
if len(args) != 1 {
return fmt.Errorf("Usage: tailscale cert [flags] <domain>")
}
domain := args[0]
if certArgs.certFile == "" {
certArgs.certFile = domain + ".crt"
}
if certArgs.keyFile == "" {
certArgs.keyFile = domain + ".key"
}
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
if err != nil {
return err
}
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
if err != nil {
return err
}
if certChanged {
fmt.Printf("Wrote public cert to %v\n", certArgs.certFile)
} else {
fmt.Printf("Public cert unchanged at %v\n", certArgs.certFile)
}
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
if err != nil {
return err
}
if keyChanged {
fmt.Printf("Wrote private key to %v\n", certArgs.keyFile)
} else {
fmt.Printf("Private key unchanged at %v\n", certArgs.keyFile)
}
return nil
}
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
return false, nil
}
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
return false, err
}
return true, nil
}

View File

@@ -107,6 +107,7 @@ change in the future.
webCmd,
fileCmd,
bugReportCmd,
certCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser"
@@ -23,7 +22,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
"tailscale.com/tstime/mono"
"tailscale.com/util/dnsname"
)
@@ -63,7 +61,7 @@ func runStatus(ctx context.Context, args []string) error {
if statusArgs.json {
if statusArgs.active {
for peer, ps := range st.Peer {
if !peerActive(ps) {
if !ps.Active {
delete(st.Peer, peer)
}
}
@@ -131,7 +129,6 @@ func runStatus(ctx context.Context, args []string) error {
var buf bytes.Buffer
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) {
active := peerActive(ps)
f("%-15s %-20s %-12s %-7s ",
firstIPString(ps.TailscaleIPs),
dnsOrQuoteHostname(st, ps),
@@ -140,7 +137,7 @@ func runStatus(ctx context.Context, args []string) error {
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
if !active {
if !ps.Active {
if ps.ExitNode {
f("idle; exit node")
} else if anyTraffic {
@@ -179,8 +176,7 @@ func runStatus(ctx context.Context, args []string) error {
}
ipnstate.SortPeers(peers)
for _, ps := range peers {
active := peerActive(ps)
if statusArgs.active && !active {
if statusArgs.active && !ps.Active {
continue
}
printPS(ps)
@@ -190,13 +186,6 @@ func runStatus(ctx context.Context, args []string) error {
return nil
}
// peerActive reports whether ps has recent activity.
//
// TODO: have the server report this bool instead.
func peerActive(ps *ipnstate.PeerStatus) bool {
return !ps.LastWrite.IsZero() && mono.Since(ps.LastWrite) < 2*time.Minute
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {

View File

@@ -478,6 +478,13 @@ func runUp(ctx context.Context, args []string) error {
}
}
// This whole 'up' mechanism is too complicated and results in
// hairy stuff like this select. We're ultimately waiting for
// 'startingOrRunning' to be done, but even in the case where
// it succeeds, other parts may shut down concurrently so we
// need to prioritize reads from 'startingOrRunning' if it's
// readable; its send does happen before the pump mechanism
// shuts down. (Issue 2333)
select {
case <-startingOrRunning:
return nil
@@ -489,6 +496,11 @@ func runUp(ctx context.Context, args []string) error {
}
return pumpCtx.Err()
case err := <-pumpErr:
select {
case <-startingOrRunning:
return nil
default:
}
return err
}
}

View File

@@ -28,7 +28,7 @@
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile.LoginName }}
<div class="text-right truncate leading-4">
<h4 class="truncate">{{.}}</h4>
<h4 class="truncate leading-normal">{{.}}</h4>
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
</div>
{{ end }}

View File

@@ -7,7 +7,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
@@ -20,8 +20,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
@@ -46,17 +45,19 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/interfaces+
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/structs from tailscale.com/ipn+
@@ -99,9 +100,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/flate from compress/gzip
compress/gzip from net/http
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -124,10 +124,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
embed from tailscale.com/cmd/tailscale/cli
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -141,8 +137,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
expvar from tailscale.com/derp+
flag from github.com/peterbourgon/ff/v2+
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash from crypto+
hash/crc32 from compress/gzip+
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate+
@@ -169,10 +164,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
os/exec from github.com/toqueteos/webbrowser+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from tailscale.com/util/groupmember
path from debug/dwarf+
path from html/template+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from rsc.io/goversion/version+
regexp from github.com/tailscale/goupnp/httpu+
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight
sort from compress/flate+

View File

@@ -35,6 +35,7 @@ import (
)
var debugArgs struct {
ifconfig bool // print network state once and exit
monitor bool
getURL string
derpCheck string
@@ -45,6 +46,7 @@ var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
@@ -59,8 +61,11 @@ func debugMode(args []string) error {
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
if debugArgs.ifconfig {
return runMonitor(ctx, false)
}
if debugArgs.monitor {
return runMonitor(ctx)
return runMonitor(ctx, true)
}
if debugArgs.portmap {
return debugPortmap(ctx)
@@ -71,7 +76,7 @@ func debugMode(args []string) error {
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context) error {
func runMonitor(ctx context.Context, loop bool) error {
dump := func(st *interfaces.State) {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
@@ -88,8 +93,13 @@ func runMonitor(ctx context.Context) error {
log.Printf("Link monitor fired. New state:")
dump(st)
})
log.Printf("Starting link change monitor; initial state:")
if loop {
log.Printf("Starting link change monitor; initial state:")
}
dump(mon.InterfaceState())
if !loop {
return nil
}
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
@@ -206,6 +216,22 @@ func debugPortmap(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
@@ -248,6 +274,13 @@ func debugPortmap(ctx context.Context) error {
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
@@ -259,13 +292,6 @@ func debugPortmap(ctx context.Context) error {
return nil
}
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {

View File

@@ -10,6 +10,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
github.com/golang/snappy from github.com/klauspost/compress/zstd
github.com/google/btree from inet.af/netstack/tcpip/header+
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
@@ -23,13 +27,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
W github.com/pkg/errors from github.com/tailscale/certstore
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/derp+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
@@ -66,7 +73,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack+
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
@@ -80,7 +87,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/waiter from inet.af/netstack/tcpip+
inet.af/peercred from tailscale.com/ipn/ipnserver
W 💣 inet.af/wf from tailscale.com/wf
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
@@ -126,12 +132,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/ipn/ipnserver+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
tailscale.com/syncs from tailscale.com/net/interfaces+
💣 tailscale.com/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/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -140,6 +147,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/net/tstun+
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/structs from tailscale.com/control/controlclient+
@@ -149,7 +157,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/util/racebuild from tailscale.com/logpolicy
@@ -157,7 +165,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
tailscale.com/util/winutil from tailscale.com/logpolicy+
tailscale.com/version from tailscale.com/cmd/tailscaled+
tailscale.com/version/distro from tailscale.com/control/controlclient+
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
@@ -169,6 +177,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
tailscale.com/wgengine/wglog from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
@@ -207,9 +216,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/flate from compress/gzip
compress/gzip from internal/profile+
compress/zlib from debug/elf+
container/heap from inet.af/netstack/tcpip/transport/tcp
container/list from crypto/tls+
context from crypto/tls+
@@ -233,10 +241,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
embed from tailscale.com/net/dns+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -250,8 +254,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem
@@ -279,7 +282,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscaled+
os/user from github.com/godbus/dbus/v5+
path from debug/dwarf+
path from github.com/godbus/dbus/v5+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+

View File

@@ -19,6 +19,7 @@ import (
"net/http"
"net/http/pprof"
"os"
"os/exec"
"os/signal"
"runtime"
"runtime/debug"
@@ -33,7 +34,6 @@ import (
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/socks5/tssocks"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
@@ -68,9 +68,13 @@ func defaultTunName() string {
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
// or comma-separated list thereof.
tunname string
cleanup bool
debug string
tunname string // tun name, "userspace-networking", or comma-separated list thereof
port uint16
statepath string
socketpath string
@@ -138,7 +142,7 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
}
@@ -159,6 +163,34 @@ func main() {
}
}
func trySynologyMigration(p string) error {
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
return nil
}
fi, err := os.Stat(p)
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
return err
}
// File is empty or doesn't exist, try reading from the old path.
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
if _, err := os.Stat(oldPath); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
return err
}
if err := os.Rename(oldPath, p); err != nil {
return err
}
return nil
}
func ipnServerOpts() (o ipnserver.Options) {
// Allow changing the OS-specific IPN behavior for tests
// so we can e.g. test Windows-specific behaviors on Linux.
@@ -221,6 +253,9 @@ func run() error {
if args.statepath == "" {
log.Fatalf("--state is required")
}
if err := trySynologyMigration(args.statepath); err != nil {
log.Printf("error in synology migration: %v", err)
}
var debugMux *http.ServeMux
if args.debug != "" {
@@ -331,9 +366,9 @@ func shouldWrapNetstack() bool {
return true
}
switch runtime.GOOS {
case "windows", "darwin":
case "windows", "darwin", "freebsd":
// Enable on Windows and tailscaled-on-macOS (this doesn't
// affect the GUI clients).
// affect the GUI clients), and on FreeBSD.
return true
}
return false
@@ -346,18 +381,33 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
}
useNetstack = name == "userspace-networking"
if !useNetstack {
dev, devName, err := tstun.New(logf, name)
// dev, devName, err := tstun.New(logf, name)
// if err != nil {
// tstun.Diagnose(logf, name)
// return nil, false, err
// }
// conf.Tun = dev
// if strings.HasPrefix(name, "tap:") {
// conf.IsTAP = true
// e, err := wgengine.NewUserspaceEngine(logf, conf)
// return e, false, err
// }
// HACK
exec.Command("ip", "link", "del", "tailscale0").Run()
if err := exec.Command("ip", "link", "add", "tailscale0", "type", "wireguard").Run(); err != nil {
return nil, false, fmt.Errorf("create device: %v", err)
}
if err := exec.Command("ip", "link", "set", "tailscale0", "up").Run(); err != nil {
return nil, false, fmt.Errorf("create device: %v", err)
}
r, err := router.New(logf, nil, linkMon)
if err != nil {
tstun.Diagnose(logf, name)
//dev.Close()
return nil, false, err
}
conf.Tun = dev
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, err
}
d, err := dns.NewOSConfigurator(logf, devName)
d, err := dns.NewOSConfigurator(logf, "tailscale0")
if err != nil {
return nil, false, err
}
@@ -367,7 +417,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}
e, err = wgengine.NewUserspaceEngine(logf, conf)
e, err = wgengine.NewKernelEngine(logf, conf)
if err != nil {
return nil, useNetstack, err
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package main // import "tailscale.com/cmd/tailscaled"

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program testcontrol runs a simple test control server.
package main
import (
"flag"
"log"
"net/http"
"testing"
"tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/logger"
)
var (
flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network")
)
func main() {
flag.Parse()
var t fakeTB
derpMap := integration.RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
control := &testcontrol.Server{
DERPMap: derpMap,
ExplicitBaseURL: "http://127.0.0.1:9911",
}
for i := 0; i < *flagNFake; i++ {
control.AddFakeNode()
}
mux := http.NewServeMux()
mux.Handle("/", control)
addr := "127.0.0.1:9911"
log.Printf("listening on %s", addr)
err := http.ListenAndServe(addr, mux)
log.Fatal(err)
}
type fakeTB struct {
*testing.T
}
func (t fakeTB) Cleanup(_ func()) {}
func (t fakeTB) Error(args ...interface{}) {
t.Fatal(args...)
}
func (t fakeTB) Errorf(format string, args ...interface{}) {
t.Fatalf(format, args...)
}
func (t fakeTB) Fail() {
t.Fatal("failed")
}
func (t fakeTB) FailNow() {
t.Fatal("failed")
}
func (t fakeTB) Failed() bool {
return false
}
func (t fakeTB) Fatal(args ...interface{}) {
log.Fatal(args...)
}
func (t fakeTB) Fatalf(format string, args ...interface{}) {
log.Fatalf(format, args...)
}
func (t fakeTB) Helper() {}
func (t fakeTB) Log(args ...interface{}) {
log.Print(args...)
}
func (t fakeTB) Logf(format string, args ...interface{}) {
log.Printf(format, args...)
}
func (t fakeTB) Name() string {
return "faketest"
}
func (t fakeTB) Setenv(key string, value string) {
panic("not implemented")
}
func (t fakeTB) Skip(args ...interface{}) {
t.Fatal("skipped")
}
func (t fakeTB) SkipNow() {
t.Fatal("skipnow")
}
func (t fakeTB) Skipf(format string, args ...interface{}) {
t.Logf(format, args...)
t.Fatal("skipped")
}
func (t fakeTB) Skipped() bool {
return false
}
func (t fakeTB) TempDir() string {
panic("not implemented")
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
// The tsshd binary is an SSH server that accepts connections

View File

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

View File

@@ -75,10 +75,3 @@ func TestStatusEqual(t *testing.T) {
}
}
}
func TestOSVersion(t *testing.T) {
if osVersion == nil {
t.Skip("not available for OS")
}
t.Logf("Got: %#q", osVersion())
}

View File

@@ -20,7 +20,6 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
@@ -33,6 +32,7 @@ import (
"inet.af/netaddr"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
@@ -47,9 +47,7 @@ import (
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/monitor"
)
@@ -184,53 +182,13 @@ func NewDirect(opts Options) (*Direct, error) {
pinger: opts.Pinger,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
c.SetHostinfo(hostinfo.New())
} else {
c.SetHostinfo(opts.Hostinfo)
}
return c, nil
}
var osVersion func() string // non-nil on some platforms
func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
var osv string
if osVersion != nil {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.Long,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
Package: packageType(),
GoArch: runtime.GOARCH,
}
}
func packageType() string {
switch runtime.GOOS {
case "windows":
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
case "darwin":
// Using tailscaled or IPNExtension?
exe, _ := os.Executable()
return filepath.Base(exe)
case "linux":
// Report whether this is in a snap.
// See https://snapcraft.io/docs/environment-variables
// We just look at two somewhat arbitrarily.
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
return "snap"
}
}
return ""
}
// SetHostinfo clones the provided Hostinfo and remembers it for the
// next update. It reports whether the Hostinfo has changed.
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
@@ -865,22 +823,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
// Get latest localPort. This might've changed if
// a lite map update occured meanwhile. This only affects
// a lite map update occurred meanwhile. This only affects
// the end-to-end test.
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
c.mu.Lock()
nm.LocalPort = c.localPort
c.mu.Unlock()
// Printing the netmap can be extremely verbose, but is very
// handy for debugging. Let's limit how often we do it.
// Code elsewhere prints netmap diffs every time, so this
// occasional full dump, plus incremental diffs, should do
// the job.
// Occasionally print the netmap header.
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
now := c.timeNow()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
}
c.mu.Lock()
@@ -1303,3 +1260,59 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
return nil
}
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
// with ping response data.
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
var err error
if pr.URL == "" {
return errors.New("invalid PingRequest with no URL")
}
if pr.IP.IsZero() {
return errors.New("PingRequest without IP")
}
if !strings.Contains(pr.Types, "TSMP") {
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
}
now := time.Now()
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
// Currently does not check for error since we just return if it fails.
err = postPingResult(now, logf, c, pr, res)
})
return err
}
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
if res.Err != "" {
return errors.New(res.Err)
}
duration := time.Since(now)
if pr.Log {
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
jsonPingRes, err := json.Marshal(res)
if err != nil {
return err
}
// Send the results of the Ping, back to control URL.
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
if err != nil {
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
}
if pr.Log {
logf("tsmpPing: sending ping results to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
}
return nil
}

View File

@@ -6,15 +6,20 @@ package controlclient
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
)
func TestNewDirect(t *testing.T) {
hi := NewHostinfo()
hi := hostinfo.New()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
@@ -56,7 +61,7 @@ func TestNewDirect(t *testing.T) {
if changed {
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
}
hi = NewHostinfo()
hi = hostinfo.New()
hi.Hostname = "different host name"
changed = c.SetHostinfo(hi)
if !changed {
@@ -92,14 +97,55 @@ func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
return
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
func TestTsmpPing(t *testing.T) {
hi := hostinfo.New()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
key, err := wgkey.NewPrivate()
if err != nil {
t.Error(err)
}
j, err := json.MarshalIndent(hi, " ", "")
opts := Options{
ServerURL: "https://example.com",
Hostinfo: hi,
GetMachinePrivateKey: func() (wgkey.Private, error) {
return key, nil
},
}
c, err := NewDirect(opts)
if err != nil {
t.Fatal(err)
}
pingRes := &ipnstate.PingResult{
IP: "123.456.7890",
Err: "",
NodeName: "testnode",
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body := new(ipnstate.PingResult)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if pingRes.IP != body.IP {
t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP)
}
w.WriteHeader(200)
}))
defer ts.Close()
now := time.Now()
pr := &tailcfg.PingRequest{
URL: ts.URL,
}
err = postPingResult(now, t.Logf, c.httpc, pr, pingRes)
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows && cgo
// +build windows,cgo
// darwin,cgo is also supported by certstore but machineCertificateSubject will

View File

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

View File

@@ -43,6 +43,7 @@ import (
"tailscale.com/metrics"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/pad32"
"tailscale.com/version"
)
@@ -76,13 +77,6 @@ const (
writeTimeout = 2 * time.Second
)
const host64bit = (^uint(0) >> 32) & 1 // 1 on 64-bit, 0 on 32-bit
// pad32bit is 4 on 32-bit machines and 0 on 64-bit.
// It exists so the Server struct's atomic fields can be aligned to 8
// byte boundaries. (As tested by GOARCH=386 go test, etc)
const pad32bit = 4 - host64bit*4 // 0 on 64-bit, 4 on 32-bit
// Server is a DERP server.
type Server struct {
// WriteTimeout, if non-zero, specifies how long to wait
@@ -98,20 +92,20 @@ type Server struct {
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
// Counters:
_ [pad32bit]byte
_ pad32.Four
packetsSent, bytesSent expvar.Int
packetsRecv, bytesRecv expvar.Int
packetsRecvByKind metrics.LabelMap
packetsRecvDisco *expvar.Int
packetsRecvOther *expvar.Int
_ [pad32bit]byte
_ pad32.Four
packetsDropped expvar.Int
packetsDroppedReason metrics.LabelMap
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
packetsDroppedType metrics.LabelMap
packetsDroppedTypeDisco *expvar.Int
packetsDroppedTypeOther *expvar.Int
_ [pad32bit]byte
_ pad32.Four
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneFrames expvar.Int // number of peer gone frames sent
@@ -156,7 +150,7 @@ type Server struct {
// PacketForwarder is something that can forward packets.
//
// It's mostly an inteface for circular dependency reasons; the
// It's mostly an interface for circular dependency reasons; the
// typical implementation is derphttp.Client. The other implementation
// is a multiForwarder, which this package creates as needed if a
// public key gets more than one PacketForwarder registered for it.
@@ -1281,7 +1275,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
return
}
if m, ok := prev.(multiForwarder); ok {
if _, ok := m[fwd]; !ok {
if _, ok := m[fwd]; ok {
// Duplicate registration of same forwarder in set; ignore.
return
}

View File

@@ -712,6 +712,7 @@ func TestForwarderRegistration(t *testing.T) {
// Adding a dup for a user.
wantCounter(&s.multiForwarderCreated, 0)
s.AddPacketForwarder(u1, testFwd(100))
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
want(map[key.Public]PacketForwarder{
u1: multiForwarder{
testFwd(1): 1,

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gofuzz
// +build gofuzz
package disco

7
go.mod
View File

@@ -19,6 +19,7 @@ require (
github.com/google/uuid v1.1.2
github.com/goreleaser/nfpm v1.10.3
github.com/iancoleman/strcase v0.2.0
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.12.2
@@ -31,7 +32,7 @@ require (
github.com/pkg/sftp v1.13.0
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
@@ -39,16 +40,16 @@ require (
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
golang.org/x/tools v0.1.2
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c // indirect
golang.zx2c4.com/wireguard/windows v0.3.16
honnef.co/go/tools v0.1.4
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
rsc.io/goversion v1.2.0
)

31
go.sum
View File

@@ -102,6 +102,7 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
@@ -297,6 +298,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@@ -304,6 +306,8 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e h1:sgh63o+pm5kcdrgyYaCIoeD7mccyL6MscVmy+DvY6C4=
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -326,6 +330,7 @@ github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/rasw
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
@@ -391,6 +396,7 @@ github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpe
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8=
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
@@ -406,6 +412,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI=
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks=
@@ -413,6 +421,7 @@ github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -581,8 +590,8 @@ github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3 h1:fEubocuQkrl
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2 h1:AIJ8AF9O7jBmCwilP0ydwJMIzW5dw48Us8f3hLJhYBY=
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
@@ -604,6 +613,8 @@ github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLK
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
@@ -656,6 +667,7 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -699,6 +711,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@@ -722,6 +735,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -755,9 +769,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -777,6 +793,7 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -790,14 +807,17 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo=
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
@@ -876,8 +896,11 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0 h1:qINUmOnDCCF7i14oomDDkGmlda7BSDTGfge77/aqdfk=
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
golang.zx2c4.com/wireguard/windows v0.3.16 h1:S42i0kp3SFHZm1mMFTtiU3OnEQJ0GRVOVlMkBhSDTZI=
golang.zx2c4.com/wireguard/windows v0.3.16/go.mod h1:f80rkFY2CKQklps1GHE15k/M4Tq78aofbr1iQM5MTVY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -973,5 +996,3 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jC
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=

View File

@@ -4,20 +4,64 @@
// Package hostinfo answers questions about the host environment that Tailscale is
// running on.
//
// TODO(bradfitz): move more of control/controlclient/hostinfo_* into this package.
package hostinfo
import (
"io"
"os"
"path/filepath"
"runtime"
"sync/atomic"
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/version"
)
var osVersion func() string // non-nil on some platforms
// New returns a partially populated Hostinfo for the current host.
func New() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
var osv string
if osVersion != nil {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.Long,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
Package: packageType(),
GoArch: runtime.GOARCH,
DeviceModel: deviceModel(),
}
}
func packageType() string {
switch runtime.GOOS {
case "windows":
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
case "darwin":
// Using tailscaled or IPNExtension?
exe, _ := os.Executable()
return filepath.Base(exe)
case "linux":
// Report whether this is in a snap.
// See https://snapcraft.io/docs/environment-variables
// We just look at two somewhat arbitrarily.
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
return "snap"
}
}
return ""
}
// EnvType represents a known environment type.
// The empty string, the default, means unknown.
type EnvType string
@@ -28,6 +72,7 @@ const (
Heroku = EnvType("hr")
AzureAppService = EnvType("az")
AWSFargate = EnvType("fg")
FlyDotIo = EnvType("fly")
)
var envType atomic.Value // of EnvType
@@ -41,6 +86,16 @@ func GetEnvType() EnvType {
return e
}
var deviceModelAtomic atomic.Value // of string
// SetDeviceModel sets the device model for use in Hostinfo updates.
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
func deviceModel() string {
s, _ := deviceModelAtomic.Load().(string)
return s
}
func getEnvType() EnvType {
if inKnative() {
return KNative
@@ -57,11 +112,14 @@ func getEnvType() EnvType {
if inAWSFargate() {
return AWSFargate
}
if inFlyDotIo() {
return FlyDotIo
}
return ""
}
// InContainer reports whether we're running in a container.
func InContainer() bool {
// inContainer reports whether we're running in a container.
func inContainer() bool {
if runtime.GOOS != "linux" {
return false
}
@@ -126,3 +184,10 @@ func inAWSFargate() bool {
}
return false
}
func inFlyDotIo() bool {
if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
return true
}
return false
}

View File

@@ -2,24 +2,31 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !android
// +build linux,!android
package controlclient
package hostinfo
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"
"syscall"
"tailscale.com/hostinfo"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
func init() {
osVersion = osVersionLinux
if v, _ := os.ReadFile("/sys/firmware/devicetree/base/model"); len(v) > 0 {
// Look up "Raspberry Pi 4 Model B Rev 1.2",
// etc. Usually set on ARM SBCs.
SetDeviceModel(strings.Trim(string(v), "\x00\r\n\t "))
}
}
func osVersionLinux() string {
@@ -54,10 +61,10 @@ func osVersionLinux() string {
}
attrBuf.WriteByte(byte(b))
}
if hostinfo.InContainer() {
if inContainer() {
attrBuf.WriteString("; container")
}
if env := hostinfo.GetEnvType(); env != "" {
if env := GetEnvType(); env != "" {
fmt.Fprintf(&attrBuf, "; env=%s", env)
}
attr := attrBuf.String()

29
hostinfo/hostinfo_test.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hostinfo
import (
"encoding/json"
"testing"
)
func TestNew(t *testing.T) {
hi := New()
if hi == nil {
t.Fatal("no Hostinfo")
}
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}
func TestOSVersion(t *testing.T) {
if osVersion == nil {
t.Skip("not available for OS")
}
t.Logf("Got: %#q", osVersion())
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
package hostinfo
import (
"os/exec"

View File

@@ -29,6 +29,7 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
@@ -38,6 +39,7 @@ import (
"tailscale.com/paths"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -729,7 +731,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return nil
}
hostinfo := controlclient.NewHostinfo()
hostinfo := hostinfo.New()
hostinfo.BackendLogID = b.backendLogID
hostinfo.FrontendLogID = opts.FrontendLogID
@@ -782,8 +784,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.inServerMode = b.prefs.ForceDaemon
b.serverURL = b.prefs.ControlURLOrDefault()
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
if b.inServerMode || runtime.GOOS == "windows" {
b.logf("Start: serverMode=%v", b.inServerMode)
}
@@ -850,6 +850,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
DiscoPublicKey: discoPublic,
DebugFlags: debugFlags,
LinkMonitor: b.e.GetLinkMonitor(),
Pinger: b.e,
// Don't warn about broken Linux IP forwading when
// netstack is being used.
@@ -960,7 +961,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
b.logf("netmap packet filter: (shields up)")
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
} else {
b.logf("netmap packet filter: %v", packetFilter)
b.logf("netmap packet filter: %v filters", len(packetFilter))
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
}
}
@@ -1578,7 +1579,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
oldHi := b.hostinfo
newHi := oldHi.Clone()
newHi.RoutableIPs = append([]netaddr.IPPrefix(nil), b.prefs.AdvertiseRoutes...)
applyPrefsToHostinfo(newHi, newp)
b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi)
@@ -1820,7 +1820,7 @@ func (b *LocalBackend) authReconfig() {
}
if uc.CorpDNS {
addDefault := func(resolvers []tailcfg.DNSResolver) {
addDefault := func(resolvers []dnstype.Resolver) {
for _, resolver := range resolvers {
res, err := parseResolver(resolver)
if err != nil {
@@ -1837,6 +1837,17 @@ func (b *LocalBackend) authReconfig() {
if err != nil {
b.logf("[unexpected] non-FQDN route suffix %q", suffix)
}
// Create map entry even if len(resolvers) == 0; Issue 2706.
// This lets the control plane send ExtraRecords for which we
// can authoritatively answer "name not exists" for when the
// control plane also sends this explicit but empty route
// making it as something we handle.
//
// While we're already populating it, might as well size the
// slice appropriately.
dcfg.Routes[fqdn] = make([]netaddr.IPPort, 0, len(resolvers))
for _, resolver := range resolvers {
res, err := parseResolver(resolver)
if err != nil {
@@ -1896,7 +1907,7 @@ func (b *LocalBackend) authReconfig() {
b.initPeerAPIListener()
}
func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) {
func parseResolver(cfg dnstype.Resolver) (netaddr.IPPort, error) {
ip, err := netaddr.ParseIP(cfg.Addr)
if err != nil {
return netaddr.IPPort{}, fmt.Errorf("[unexpected] non-IP resolver %q", cfg.Addr)
@@ -2216,6 +2227,8 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
if m := prefs.DeviceModel; m != "" {
hi.DeviceModel = m
}
hi.RoutableIPs = append(prefs.AdvertiseRoutes[:0:0], prefs.AdvertiseRoutes...)
hi.RequestTags = append(prefs.AdvertiseTags[:0:0], prefs.AdvertiseTags...)
hi.ShieldsUp = prefs.ShieldsUp
}

View File

@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,ts_macext ios,ts_macext
//go:build ts_macext && (darwin || ios)
// +build ts_macext
// +build darwin ios
package ipnlocal

View File

@@ -20,7 +20,6 @@ import (
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
)
@@ -91,12 +90,19 @@ type PeerStatus struct {
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
LastWrite mono.Time // time last packet sent
LastWrite time.Time // time last packet sent
LastSeen time.Time // last seen to tailcontrol
LastHandshake time.Time // with local wireguard
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
// currently means that there was some packet sent to this
// peer in the past two minutes. That definition is subject to
// change.
Active bool
PeerAPIURL []string
Capabilities []string `json:",omitempty"`
@@ -278,6 +284,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if st.ShareeNode {
e.ShareeNode = true
}
if st.Active {
e.Active = true
}
}
type StatusUpdater interface {
@@ -321,7 +330,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
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 := mono.Now()
now := time.Now()
var peers []*PeerStatus
for _, peer := range st.Peers() {
@@ -378,9 +387,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
)
f("<td>")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && mono.Since(ps.LastWrite) < 2*time.Minute
if active {
if ps.Active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {

450
ipn/localapi/cert.go Normal file
View File

@@ -0,0 +1,450 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios && !android
// +build !ios,!android
package localapi
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/acme"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/types/logger"
)
// Process-wide cache. (A new *Handler is created per connection,
// effectively per request)
var (
// acmeMu guards all ACME operations, so concurrent requests
// for certs don't slam ACME. The first will go through and
// populate the on-disk cache and the rest should use that.
acmeMu sync.Mutex
renewMu sync.Mutex // lock order: don't hold acmeMu and renewMu at the same time
lastRenewCheck = map[string]time.Time{}
)
func (h *Handler) certDir() (string, error) {
base := paths.DefaultTailscaledStateFile()
if base == "" {
return "", errors.New("no default DefaultTailscaledStateFile")
}
full := filepath.Join(filepath.Dir(base), "certs")
if err := os.MkdirAll(full, 0700); err != nil {
return "", err
}
return full, nil
}
var acmeDebug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ACME"))
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "cert access denied", http.StatusForbidden)
return
}
dir, err := h.certDir()
if err != nil {
h.logf("certDir: %v", err)
http.Error(w, "failed to get cert dir", 500)
return
}
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
if domain == r.URL.Path {
http.Error(w, "internal handler config wired wrong", 500)
return
}
now := time.Now()
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
traceACME := func(v interface{}) {
if !acmeDebug {
return
}
j, _ := json.MarshalIndent(v, "", "\t")
log.Printf("acme %T: %s", v, j)
}
if pair, ok := h.getCertPEMCached(dir, domain, now); ok {
future := now.AddDate(0, 0, 14)
if h.shouldStartDomainRenewal(dir, domain, future) {
logf("starting async renewal")
// Start renewal in the background.
go h.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
}
serveKeyPair(w, r, pair)
return
}
pair, err := h.getCertPEM(r.Context(), logf, traceACME, dir, domain, now)
if err != nil {
logf("getCertPEM: %v", err)
http.Error(w, fmt.Sprint(err), 500)
return
}
serveKeyPair(w, r, pair)
}
func (h *Handler) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
renewMu.Lock()
defer renewMu.Unlock()
now := time.Now()
if last, ok := lastRenewCheck[domain]; ok && now.Sub(last) < time.Minute {
// We checked very recently. Don't bother reparsing &
// validating the x509 cert.
return false
}
lastRenewCheck[domain] = now
_, ok := h.getCertPEMCached(dir, domain, future)
return !ok
}
func serveKeyPair(w http.ResponseWriter, r *http.Request, p *keyPair) {
w.Header().Set("Content-Type", "text/plain")
switch r.URL.Query().Get("type") {
case "", "crt", "cert":
w.Write(p.certPEM)
case "key":
w.Write(p.keyPEM)
case "pair":
w.Write(p.keyPEM)
w.Write(p.certPEM)
default:
http.Error(w, `invalid type; want "cert" (default), "key", or "pair"`, 400)
}
}
type keyPair struct {
certPEM []byte
keyPEM []byte
cached bool
}
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
// getCertPEMCached returns a non-nil keyPair and true if a cached
// keypair for domain exists on disk in dir that is valid at the
// provided now time.
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
certPEM, _ := os.ReadFile(certFile(dir, domain))
if validCertPEM(domain, keyPEM, certPEM, now) {
return &keyPair{certPEM: certPEM, keyPEM: keyPEM, cached: true}, true
}
}
return nil, false
}
func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(interface{}), dir, domain string, now time.Time) (*keyPair, error) {
acmeMu.Lock()
defer acmeMu.Unlock()
if p, ok := h.getCertPEMCached(dir, domain, now); ok {
return p, nil
}
key, err := acmeKey(dir)
if err != nil {
return nil, fmt.Errorf("acmeKey: %w", err)
}
ac := &acme.Client{Key: key}
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
switch {
case err == nil:
// Great, already registered.
logf("already had ACME account.")
case err == acme.ErrNoAccount:
a, err = ac.Register(ctx, new(acme.Account), acme.AcceptTOS)
if err == acme.ErrAccountAlreadyExists {
// Potential race. Double check.
a, err = ac.GetReg(ctx, "" /* pre-RFC param */)
}
if err != nil {
return nil, fmt.Errorf("acme.Register: %w", err)
}
logf("registered ACME account.")
traceACME(a)
default:
return nil, fmt.Errorf("acme.GetReg: %w", err)
}
if a.Status != acme.StatusValid {
return nil, fmt.Errorf("unexpected ACME account status %q", a.Status)
}
// Before hitting LetsEncrypt, see if this is a domain that Tailscale will do DNS challenges for.
st := h.b.StatusWithoutPeers()
if err := checkCertDomain(st, domain); err != nil {
return nil, err
}
order, err := ac.AuthorizeOrder(ctx, []acme.AuthzID{{Type: "dns", Value: domain}})
if err != nil {
return nil, err
}
traceACME(order)
for _, aurl := range order.AuthzURLs {
az, err := ac.GetAuthorization(ctx, aurl)
if err != nil {
return nil, err
}
traceACME(az)
for _, ch := range az.Challenges {
if ch.Type == "dns-01" {
rec, err := ac.DNS01ChallengeRecord(ch.Token)
if err != nil {
return nil, err
}
key := "_acme-challenge." + domain
var resolver net.Resolver
var ok bool
txts, _ := resolver.LookupTXT(ctx, key)
for _, txt := range txts {
if txt == rec {
ok = true
logf("TXT record already existed")
break
}
}
if !ok {
err = h.b.SetDNS(ctx, key, rec)
if err != nil {
return nil, fmt.Errorf("SetDNS %q => %q: %w", key, rec, err)
}
logf("did SetDNS")
}
chal, err := ac.Accept(ctx, ch)
if err != nil {
return nil, fmt.Errorf("Accept: %v", err)
}
traceACME(chal)
break
}
}
}
wait0 := time.Now()
orderURI := order.URI
for {
order, err = ac.WaitOrder(ctx, orderURI)
if err == nil {
break
}
if oe, ok := err.(*acme.OrderError); ok && oe.Status == acme.StatusInvalid {
if time.Since(wait0) > 2*time.Minute {
return nil, errors.New("timeout waiting for order to not be invalid")
}
log.Printf("order invalid; waiting...")
select {
case <-time.After(5 * time.Second):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, fmt.Errorf("WaitOrder: %v", err)
}
traceACME(order)
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
var privPEM bytes.Buffer
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
return nil, err
}
csr, err := certRequest(certPrivKey, domain, nil)
if err != nil {
return nil, err
}
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
if err != nil {
return nil, fmt.Errorf("CreateOrder: %v", err)
}
var certPEM bytes.Buffer
for _, b := range der {
pb := &pem.Block{Type: "CERTIFICATE", Bytes: b}
if err := pem.Encode(&certPEM, pb); err != nil {
return nil, err
}
}
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
return nil, err
}
return &keyPair{certPEM: certPEM.Bytes(), keyPEM: privPEM.Bytes()}, nil
}
// certRequest generates a CSR for the given common name cn and optional SANs.
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
req := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: cn},
DNSNames: san,
ExtraExtensions: ext,
}
return x509.CreateCertificateRequest(rand.Reader, req, key)
}
func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
b, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
return pem.Encode(w, pb)
}
// parsePrivateKey is a copy of x/crypto/acme's parsePrivateKey.
//
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
//
// Inspired by parsePrivateKey in crypto/tls/tls.go.
func parsePrivateKey(der []byte) (crypto.Signer, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
case *ecdsa.PrivateKey:
return key, nil
default:
return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}
return nil, errors.New("acme/autocert: failed to parse private key")
}
func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := ioutil.ReadFile(pemName); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
}
return parsePrivateKey(priv.Bytes)
}
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
var pemBuf bytes.Buffer
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
return nil, err
}
return privKey, nil
}
func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
if len(keyPEM) == 0 || len(certPEM) == 0 {
return false
}
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return false
}
var leaf *x509.Certificate
intermediates := x509.NewCertPool()
for i, certDER := range tlsCert.Certificate {
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return false
}
if i == 0 {
leaf = cert
} else {
intermediates.AddCert(cert)
}
}
if leaf == nil {
return false
}
_, err = leaf.Verify(x509.VerifyOptions{
DNSName: domain,
CurrentTime: now,
Intermediates: intermediates,
})
return err == nil
}
func checkCertDomain(st *ipnstate.Status, domain string) error {
if domain == "" {
return errors.New("missing domain name")
}
for _, d := range st.CertDomains {
if d == domain {
return nil
}
}
// Transitional way while server doesn't yet populate CertDomains: also permit the client
// attempting Self.DNSName.
okay := st.CertDomains[:len(st.CertDomains):len(st.CertDomains)]
if st.Self != nil {
if v := strings.Trim(st.Self.DNSName, "."); v != "" {
if v == domain {
return nil
}
okay = append(okay, v)
}
}
switch len(okay) {
case 0:
return errors.New("your Tailscale account does not support getting TLS certs")
case 1:
return fmt.Errorf("invalid domain %q; only %q is permitted", domain, okay[0])
default:
return fmt.Errorf("invalid domain %q; must be one of %q", domain, okay)
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ios || android
// +build ios android
package localapi
import (
"net/http"
"runtime"
)
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
http.Error(w, "disabled on "+runtime.GOOS, http.StatusNotFound)
}

View File

@@ -30,6 +30,7 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/version"
)
func randHex(n int) string {
@@ -64,6 +65,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server has no local backend", http.StatusInternalServerError)
return
}
w.Header().Set("Tailscale-Version", version.Long)
if h.RequiredPassword != "" {
_, pass, ok := r.BasicAuth()
if !ok {
@@ -83,6 +85,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveFilePut(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
h.serveCert(w, r)
return
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)

View File

@@ -30,6 +30,15 @@ type Filch struct {
alt *os.File
altscan *bufio.Scanner
recovered int64
// buf is an initial buffer for altscan.
// As of August 2021, 99.96% of all log lines
// are below 4096 bytes in length.
// Since this cutoff is arbitrary, instead of using 4096,
// we subtract off the size of the rest of the struct
// so that the whole struct takes 4096 bytes
// (less on 32 bit platforms).
// This reduces allocation waste.
buf [4096 - 48]byte
}
// TryReadline implements the logtail.Buffer interface.
@@ -53,6 +62,7 @@ func (f *Filch) TryReadLine() ([]byte, error) {
return nil, err
}
f.altscan = bufio.NewScanner(f.alt)
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
f.altscan.Split(splitLines)
return f.scan()
}
@@ -188,6 +198,7 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
}
if f.recovered > 0 {
f.altscan = bufio.NewScanner(f.alt)
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
f.altscan.Split(splitLines)
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"testing"
"unicode"
"unsafe"
)
type filchTest struct {
@@ -169,3 +170,10 @@ func TestFilchStderr(t *testing.T) {
t.Errorf("unexpected write to fake stderr: %s", b)
}
}
func TestSizeOf(t *testing.T) {
s := unsafe.Sizeof(Filch{})
if s > 4096 {
t.Fatalf("Filch{} has size %d on %v, decrease size of buf field", s, runtime.GOARCH)
}
}

View File

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

View File

@@ -200,9 +200,11 @@ func (l *Logger) drainBlock() (shuttingDown bool) {
}
// drainPending drains and encodes a batch of logs from the buffer for upload.
// It uses scratch as its initial buffer.
// If no logs are available, drainPending blocks until logs are available.
func (l *Logger) drainPending() (res []byte) {
buf := new(bytes.Buffer)
func (l *Logger) drainPending(scratch []byte) (res []byte) {
buf := bytes.NewBuffer(scratch[:0])
buf.WriteByte('[')
entries := 0
var batchDone bool
@@ -242,28 +244,15 @@ func (l *Logger) drainPending() (res []byte) {
b = l.encodeText(b, true)
}
switch {
case entries == 0:
buf.Write(b)
case entries == 1:
buf2 := new(bytes.Buffer)
buf2.WriteByte('[')
buf2.Write(buf.Bytes())
buf2.WriteByte(',')
buf2.Write(b)
buf.Reset()
buf.Write(buf2.Bytes())
default:
if entries > 0 {
buf.WriteByte(',')
buf.Write(b)
}
buf.Write(b)
entries++
}
if entries > 1 {
buf.WriteByte(']')
}
if buf.Len() == 0 {
buf.WriteByte(']')
if buf.Len() <= len("[]") {
return nil
}
return buf.Bytes()
@@ -273,8 +262,9 @@ func (l *Logger) drainPending() (res []byte) {
func (l *Logger) uploading(ctx context.Context) {
defer close(l.shutdownDone)
scratch := make([]byte, 4096) // reusable buffer to write into
for {
body := l.drainPending()
body := l.drainPending(scratch)
origlen := -1 // sentinel value: uncompressed
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
if l.zstdEncoder != nil && len(body) > 256 {

View File

@@ -117,12 +117,7 @@ func TestEncodeAndUploadMessages(t *testing.T) {
io.WriteString(l, tt.log)
body := <-ts.uploaded
data := make(map[string]interface{})
err := json.Unmarshal(body, &data)
if err != nil {
t.Error(err)
}
data := unmarshalOne(t, body)
got := data["text"]
if got != tt.want {
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
@@ -154,11 +149,7 @@ func TestEncodeSpecialCases(t *testing.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)
}
data := unmarshalOne(t, body)
errorHasLogtail, ok := data["error_has_logtail"]
if ok {
if errorHasLogtail != "LOGTAIL" {
@@ -186,11 +177,7 @@ func TestEncodeSpecialCases(t *testing.T) {
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)
}
data = unmarshalOne(t, body)
_, ok = data["logtail"]
if ok {
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
@@ -204,11 +191,7 @@ func TestEncodeSpecialCases(t *testing.T) {
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)
}
data = unmarshalOne(t, body)
text, ok := data["text"]
if !ok {
t.Errorf("lowMem: no text %v", data)
@@ -219,7 +202,7 @@ func TestEncodeSpecialCases(t *testing.T) {
// -------------------------------------------------------------------------
err = l.Shutdown(context.Background())
err := l.Shutdown(context.Background())
if err != nil {
t.Error(err)
}
@@ -326,3 +309,16 @@ func TestPublicIDUnmarshalText(t *testing.T) {
t.Errorf("allocs = %v; want 0", n)
}
}
func unmarshalOne(t *testing.T, body []byte) map[string]interface{} {
t.Helper()
var entries []map[string]interface{}
err := json.Unmarshal(body, &entries)
if err != nil {
t.Error(err)
}
if len(entries) != 1 {
t.Fatalf("expected one entry, got %d", len(entries))
}
return entries[0]
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || freebsd || openbsd
// +build linux freebsd openbsd
package dns

View File

@@ -216,7 +216,8 @@ func (m directManager) restoreBackup() error {
if err != nil {
return err
}
if _, err := m.fs.Stat(resolvConf); err != nil && !os.IsNotExist(err) {
_, err = m.fs.Stat(resolvConf)
if err != nil && !os.IsNotExist(err) {
return err
}
resolvConfExists := !os.IsNotExist(err)
@@ -259,7 +260,7 @@ func (m directManager) SetDNS(config OSConfig) error {
// try to manage DNS through resolved when it's around, but as a
// best-effort fallback if we messed up the detection, try to
// restart resolved to make the system configuration consistent.
if isResolvedRunning() {
if isResolvedRunning() && !runningAsGUIDesktopUser() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
}
@@ -319,7 +320,7 @@ func (m directManager) Close() error {
return err
}
if isResolvedRunning() {
if isResolvedRunning() && !runningAsGUIDesktopUser() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
}
@@ -385,3 +386,12 @@ func (fs directFS) ReadFile(name string) ([]byte, error) {
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
return ioutil.WriteFile(fs.path(name), contents, perm)
}
// runningAsGUIDesktopUser reports whether it seems that this code is
// being run as a regular user on a Linux desktop. This is a quick
// hack to fix Issue 2672 where PolicyKit pops up a GUI dialog asking
// to proceed we do a best effort attempt to restart
// systemd-resolved.service. There's surely a better way.
func runningAsGUIDesktopUser() bool {
return os.Getuid() != 0 && os.Getenv("DISPLAY") != ""
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || freebsd || openbsd
// +build linux freebsd openbsd
package dns

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || freebsd || openbsd
// +build linux freebsd openbsd
package dns

View File

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

View File

@@ -10,7 +10,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"math/rand"
@@ -65,44 +64,13 @@ func getTxID(packet []byte) txid {
}
dnsid := binary.BigEndian.Uint16(packet[0:2])
qcount := binary.BigEndian.Uint16(packet[4:6])
if qcount == 0 {
return txid(dnsid)
}
offset := headerBytes
for i := uint16(0); i < qcount; i++ {
// Note: this relies on the fact that names are not compressed in questions,
// so they are guaranteed to end with a NUL byte.
//
// Justification:
// RFC 1035 doesn't seem to explicitly prohibit compressing names in questions,
// but this is exceedingly unlikely to be done in practice. A DNS request
// with multiple questions is ill-defined (which questions do the header flags apply to?)
// and a single question would have to contain a pointer to an *answer*,
// which would be excessively smart, pointless (an answer can just as well refer to the question)
// and perhaps even prohibited: a draft RFC (draft-ietf-dnsind-local-compression-05) states:
//
// > It is important that these pointers always point backwards.
//
// This is said in summarizing RFC 1035, although that phrase does not appear in the original RFC.
// Additionally, (https://cr.yp.to/djbdns/notes.html) states:
//
// > The precise rule is that a name can be compressed if it is a response owner name,
// > the name in NS data, the name in CNAME data, the name in PTR data, the name in MX data,
// > or one of the names in SOA data.
namebytes := bytes.IndexByte(packet[offset:], 0)
// ... | name | NUL | type | class
// ?? 1 2 2
offset = offset + namebytes + 5
if len(packet) < offset {
// Corrupt packet; don't crash.
return txid(dnsid)
}
}
hash := crc32.ChecksumIEEE(packet[headerBytes:offset])
return (txid(hash) << 32) | txid(dnsid)
// Previously, we hashed the question and combined it with the original txid
// which was useful when concurrent queries were multiplexed on a single
// local source port. We encountered some situations where the DNS server
// canonicalizes the question in the response (uppercase converted to
// lowercase in this case), which resulted in responses that we couldn't
// match to the original request due to hash mismatches.
return txid(dnsid)
}
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not

View File

@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,ts_macext ios,ts_macext
//go:build ts_macext && (darwin || ios)
// +build ts_macext
// +build darwin ios
package resolver

View File

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

View File

@@ -334,7 +334,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
return netaddr.IP{}, dns.RCodeNotImplemented
// For everything except for the few types above that are explictly not implemented, return no records.
// For everything except for the few types above that are explicitly not implemented, return no records.
// This is what other DNS systems do: always return NOERROR
// without any records whenever the requested record type is unknown.
// You can try this with:

View File

@@ -6,6 +6,7 @@ package resolver
import (
"fmt"
"strings"
"testing"
"github.com/miekg/dns"
@@ -66,6 +67,58 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
}
}
// resolveToIPLowercase returns a handler function which canonicalizes responses
// by lowercasing the question and answer names, and responds
// to queries of type A it receives with an A record containing ipv4,
// to queries of type AAAA with an AAAA record containing ipv6,
// to queries of type NS with an NS record containg name.
func resolveToIPLowercase(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
return func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
if len(req.Question) != 1 {
panic("not a single-question request")
}
m.Question[0].Name = strings.ToLower(m.Question[0].Name)
question := req.Question[0]
var ans dns.RR
switch question.Qtype {
case dns.TypeA:
ans = &dns.A{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: ipv4.IPAddr().IP,
}
case dns.TypeAAAA:
ans = &dns.AAAA{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
},
AAAA: ipv6.IPAddr().IP,
}
case dns.TypeNS:
ans = &dns.NS{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
},
Ns: ns,
}
}
m.Answer = append(m.Answer, ans)
w.WriteMsg(m)
}
}
// resolveToTXT returns a handler function which responds to queries of type TXT
// it receives with the strings in txts.
func resolveToTXT(txts []string, ednsMaxSize uint16) dns.HandlerFunc {

View File

@@ -440,6 +440,8 @@ func TestDelegate(t *testing.T) {
records := []interface{}{
"test.site.",
resolveToIP(testipv4, testipv6, "dns.test.site."),
"LCtesT.SiTe.",
resolveToIPLowercase(testipv4, testipv6, "dns.test.site."),
"nxdomain.site.", resolveToNXDOMAIN,
"small.txt.", resolveToTXT(smallTXT, noEdns),
"smalledns.txt.", resolveToTXT(smallTXT, 512),
@@ -485,6 +487,21 @@ func TestDelegate(t *testing.T) {
dnspacket("test.site.", dns.TypeNS, noEdns),
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
},
{
"ipv4",
dnspacket("LCtesT.SiTe.", dns.TypeA, noEdns),
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
},
{
"ipv6",
dnspacket("LCtesT.SiTe.", dns.TypeAAAA, noEdns),
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
},
{
"ns",
dnspacket("LCtesT.SiTe.", dns.TypeNS, noEdns),
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
},
{
"nxdomain",
dnspacket("nxdomain.site.", dns.TypeA, noEdns),

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
// +build ignore
package main

View File

@@ -49,7 +49,7 @@ type entry struct {
value interface{}
}
// Add adds a value to the cache, set or updating its assoicated
// Add adds a value to the cache, set or updating its associated
// value.
//
// If MaxEntries is non-zero and the length of the cache is greater

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || (darwin && !ts_macext)
// +build linux darwin,!ts_macext
package interfaces

View File

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

View File

@@ -112,22 +112,36 @@ func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
return mtus, err
}
func notTailscaleInterface(iface *winipcfg.IPAdapterAddresses) bool {
// TODO(bradfitz): do this without the Description method's
// utf16-to-string allocation. But at least we only do it for
// the virtual interfaces, for which there won't be many.
return !(iface.IfType == winipcfg.IfTypePropVirtual &&
iface.Description() == tsconst.WintunInterfaceDesc)
}
// NonTailscaleInterfaces returns a map of interface LUID to interface
// for all interfaces except Tailscale tunnels.
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
return getInterfaces(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces, notTailscaleInterface)
}
// getInterfaces returns a map of interfaces keyed by their LUID for
// all interfaces matching the provided match predicate.
//
// The family (AF_UNSPEC, AF_INET, or AF_INET6) and flags are passed
// to winipcfg.GetAdaptersAddresses.
func getInterfaces(family winipcfg.AddressFamily, flags winipcfg.GAAFlags, match func(*winipcfg.IPAdapterAddresses) bool) (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
ifs, err := winipcfg.GetAdaptersAddresses(family, flags)
if err != nil {
return nil, err
}
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
for _, iface := range ifs {
if iface.Description() == tsconst.WintunInterfaceDesc {
continue
if match(iface) {
ret[iface.LUID] = iface
}
ret[iface.LUID] = iface
}
return ret, nil
}
@@ -135,8 +149,26 @@ func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, e
// default route for the given address family.
//
// It returns (nil, nil) if no interface is found.
//
// The family must be one of AF_INET or AF_INET6.
func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddresses, error) {
ifs, err := NonTailscaleInterfaces()
ifs, err := getInterfaces(family, winipcfg.GAAFlagIncludeAllInterfaces, func(iface *winipcfg.IPAdapterAddresses) bool {
switch iface.IfType {
case winipcfg.IfTypeSoftwareLoopback:
return false
}
switch family {
case windows.AF_INET:
if iface.Flags&winipcfg.IPAAFlagIpv4Enabled == 0 {
return false
}
case windows.AF_INET6:
if iface.Flags&winipcfg.IPAAFlagIpv6Enabled == 0 {
return false
}
}
return iface.OperStatus == winipcfg.IfOperStatusUp && notTailscaleInterface(iface)
})
if err != nil {
return nil, err
}
@@ -149,12 +181,31 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
bestMetric := ^uint32(0)
var bestIface *winipcfg.IPAdapterAddresses
for _, route := range routes {
iface := ifs[route.InterfaceLUID]
if route.DestinationPrefix.PrefixLength != 0 || iface == nil {
if route.DestinationPrefix.PrefixLength != 0 {
// Not a default route.
continue
}
if iface.OperStatus == winipcfg.IfOperStatusUp && route.Metric < bestMetric {
bestMetric = route.Metric
iface := ifs[route.InterfaceLUID]
if iface == nil {
continue
}
// Microsoft docs say:
//
// "The actual route metric used to compute the route
// preferences for IPv4 is the summation of the route
// metric offset specified in the Metric member of the
// MIB_IPFORWARD_ROW2 structure and the interface
// metric specified in this member for IPv4"
metric := route.Metric
switch family {
case windows.AF_INET:
metric += iface.Ipv4Metric
case windows.AF_INET6:
metric += iface.Ipv6Metric
}
if metric < bestMetric {
bestMetric = metric
bestIface = iface
}
}
@@ -163,6 +214,9 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
}
func DefaultRouteInterface() (string, error) {
// We always return the IPv4 default route.
// TODO(bradfitz): adjust API if/when anything cares. They could in theory differ, though,
// in which case we might send traffic to the wrong interface.
iface, err := GetWindowsDefault(windows.AF_INET)
if err != nil {
return "", err

View File

@@ -362,7 +362,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
tries = 2
} else if hadBoth {
// For dual stack machines, make the 3rd & slower nodes alternate
// beetween.
// between.
if ri%2 == 0 {
do4, do6 = true, false
} else {

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build android
// +build android
package netns

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin && !ts_macext
// +build darwin,!ts_macext
package netns

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (!linux && !windows && !darwin) || (darwin && ts_macext)
// +build !linux,!windows,!darwin darwin,ts_macext
package netns

View File

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

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || ios
// +build darwin ios
package netns

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios
// +build !ios
package netns

View File

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

View File

@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ios
// +build ios
// (https://github.com/tailscale/tailscale/issues/2495)
package portmapper
@@ -15,8 +17,10 @@ import (
type upnpClient interface{}
func getUPnPClient(ctx context.Context, gw netaddr.IP) (upnpClient, error) {
return nil, nil
type uPnPDiscoResponse struct{}
func parseUPnPDiscoResponse([]byte) (uPnPDiscoResponse, error) {
return uPnPDiscoResponse{}, nil
}
func (c *Client) getUPnPPortMapping(

254
net/portmapper/igd_test.go Normal file
View File

@@ -0,0 +1,254 @@
// 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 portmapper
import (
"bytes"
"fmt"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"inet.af/netaddr"
"tailscale.com/syncs"
"tailscale.com/types/logger"
)
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
// implementations of NAT-PMP, PCP, and/or UPnP to test clients against.
type TestIGD struct {
upnpConn net.PacketConn // for UPnP discovery
pxpConn net.PacketConn // for NAT-PMP and/or PCP
ts *httptest.Server
logf logger.Logf
closed syncs.AtomicBool
// do* will log which packets are sent, but will not reply to unexpected packets.
doPMP bool
doPCP bool
doUPnP bool
mu sync.Mutex // guards below
counters igdCounters
}
// TestIGDOptions are options
type TestIGDOptions struct {
PMP bool
PCP bool
UPnP bool // TODO: more options for 3 flavors of UPnP services
}
type igdCounters struct {
numUPnPDiscoRecv int32
numUPnPOtherUDPRecv int32
numUPnPHTTPRecv int32
numPMPRecv int32
numPMPDiscoRecv int32
numPCPRecv int32
numPCPDiscoRecv int32
numPCPMapRecv int32
numPCPOtherRecv int32
numPMPPublicAddrRecv int32
numPMPBogusRecv int32
numFailedWrites int32
invalidPCPMapPkt int32
}
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
d := &TestIGD{
logf: logf,
doPMP: t.PMP,
doPCP: t.PCP,
doUPnP: t.UPnP,
}
var err error
if d.upnpConn, err = testListenUDP(); err != nil {
return nil, err
}
if d.pxpConn, err = testListenUDP(); err != nil {
d.upnpConn.Close()
return nil, err
}
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
go d.serveUPnPDiscovery()
go d.servePxP()
return d, nil
}
func testListenUDP() (net.PacketConn, error) {
return net.ListenPacket("udp4", "127.0.0.1:0")
}
func (d *TestIGD) TestPxPPort() uint16 {
return uint16(d.pxpConn.LocalAddr().(*net.UDPAddr).Port)
}
func (d *TestIGD) TestUPnPPort() uint16 {
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
}
func testIPAndGateway() (gw, ip netaddr.IP, ok bool) {
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
}
func (d *TestIGD) Close() error {
d.closed.Set(true)
d.ts.Close()
d.upnpConn.Close()
d.pxpConn.Close()
return nil
}
func (d *TestIGD) inc(p *int32) {
d.mu.Lock()
defer d.mu.Unlock()
(*p)++
}
func (d *TestIGD) stats() igdCounters {
d.mu.Lock()
defer d.mu.Unlock()
return d.counters
}
func (d *TestIGD) serveUPnPHTTP(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) // TODO
}
func (d *TestIGD) serveUPnPDiscovery() {
buf := make([]byte, 1500)
for {
n, src, err := d.upnpConn.ReadFrom(buf)
if err != nil {
if !d.closed.Get() {
d.logf("serveUPnP failed: %v", err)
}
return
}
pkt := buf[:n]
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
d.inc(&d.counters.numUPnPDiscoRecv)
resPkt := []byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: %s\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n", d.ts.URL+"/rootDesc.xml"))
if d.doUPnP {
_, err = d.upnpConn.WriteTo(resPkt, src)
if err != nil {
d.inc(&d.counters.numFailedWrites)
}
}
} else {
d.inc(&d.counters.numUPnPOtherUDPRecv)
}
}
}
// servePxP serves NAT-PMP and PCP, which share a port number.
func (d *TestIGD) servePxP() {
buf := make([]byte, 1500)
for {
n, a, err := d.pxpConn.ReadFrom(buf)
if err != nil {
if !d.closed.Get() {
d.logf("servePxP failed: %v", err)
}
return
}
ua := a.(*net.UDPAddr)
src, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone)
if !ok {
panic("bogus addr")
}
pkt := buf[:n]
if len(pkt) < 2 {
continue
}
ver := pkt[0]
switch ver {
default:
continue
case pmpVersion:
d.handlePMPQuery(pkt, src)
case pcpVersion:
d.handlePCPQuery(pkt, src)
}
}
}
func (d *TestIGD) handlePMPQuery(pkt []byte, src netaddr.IPPort) {
d.inc(&d.counters.numPMPRecv)
if len(pkt) < 2 {
return
}
op := pkt[1]
switch op {
case pmpOpMapPublicAddr:
if len(pkt) != 2 {
d.inc(&d.counters.numPMPBogusRecv)
return
}
d.inc(&d.counters.numPMPPublicAddrRecv)
}
// TODO
}
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
d.inc(&d.counters.numPCPRecv)
if len(pkt) < 24 {
return
}
op := pkt[1]
pktSrcBytes := [16]byte{}
copy(pktSrcBytes[:], pkt[8:24])
pktSrc := netaddr.IPFrom16(pktSrcBytes)
if pktSrc != src.IP() {
// TODO this error isn't fatal but should be rejected by server.
// Since it's a test it's difficult to get them the same though.
d.logf("mismatch of packet source and source IP: got %v, expected %v", pktSrc, src.IP())
}
switch op {
case pcpOpAnnounce:
d.inc(&d.counters.numPCPDiscoRecv)
if !d.doPCP {
return
}
resp := buildPCPDiscoResponse(pkt)
if _, err := d.pxpConn.WriteTo(resp, src.UDPAddr()); err != nil {
d.inc(&d.counters.numFailedWrites)
}
case pcpOpMap:
if len(pkt) < 60 {
d.logf("got too short packet for pcp op map: %v", pkt)
d.inc(&d.counters.invalidPCPMapPkt)
return
}
d.inc(&d.counters.numPCPMapRecv)
if !d.doPCP {
return
}
resp := buildPCPMapResponse(pkt)
d.pxpConn.WriteTo(resp, src.UDPAddr())
default:
// unknown op code, ignore it for now.
d.inc(&d.counters.numPCPOtherRecv)
return
}
}
func newTestClient(t *testing.T, igd *TestIGD) *Client {
var c *Client
c = NewClient(t.Logf, func() {
t.Logf("port map changed")
t.Logf("have mapping: %v", c.HaveMapping())
})
c.testPxPPort = igd.TestPxPPort()
c.testUPnPPort = igd.TestUPnPPort()
c.SetGatewayLookupFunc(testIPAndGateway)
return c
}

157
net/portmapper/pcp.go Normal file
View File

@@ -0,0 +1,157 @@
// 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 portmapper
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"time"
"inet.af/netaddr"
)
// References:
//
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
// PCP constants
const (
pcpVersion = 2
pcpDefaultPort = 5351
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
pcpCodeOK = 0
pcpCodeNotAuthorized = 2
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
pcpUDPMapping = 17 // portmap UDP
pcpTCPMapping = 6 // portmap TCP
)
type pcpMapping struct {
c *Client
gw netaddr.IPPort
internal netaddr.IPPort
external netaddr.IPPort
renewAfter time.Time
goodUntil time.Time
// TODO should this also contain an epoch?
// Doesn't seem to be used elsewhere, but can use it for validation at some point.
}
func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
func (p *pcpMapping) Release(ctx context.Context) {
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0, p.external.IP())
uc.WriteTo(pkt, p.gw.UDPAddr())
}
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
// To create a packet which deletes a mapping, lifetimeSec should be set to 0.
// If prevPort is not known, it should be set to 0.
// If prevExternalIP is not known, it should be set to 0.0.0.0.
func buildPCPRequestMappingPacket(
myIP netaddr.IP,
localPort, prevPort uint16,
lifetimeSec uint32,
prevExternalIP netaddr.IP,
) (pkt []byte) {
// 24 byte common PCP header + 36 bytes of MAP-specific fields
pkt = make([]byte, 24+36)
pkt[0] = pcpVersion
pkt[1] = pcpOpMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec)
myIP16 := myIP.As16()
copy(pkt[8:24], myIP16[:])
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mapping nonce
// TODO: should this be a UDP mapping? It looks like it supports "all protocols" with 0, but
// also doesn't support a local port then.
mapOp[12] = pcpUDPMapping
binary.BigEndian.PutUint16(mapOp[16:18], localPort)
binary.BigEndian.PutUint16(mapOp[18:20], prevPort)
prevExternalIP16 := prevExternalIP.As16()
copy(mapOp[20:], prevExternalIP16[:])
return pkt
}
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
// In particular, its Client is not populated.
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
if len(resp) < 60 {
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
}
res, ok := parsePCPResponse(resp[:24])
if !ok {
return nil, fmt.Errorf("Invalid PCP common header")
}
if res.ResultCode != pcpCodeOK {
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
}
// TODO: don't ignore the nonce and make sure it's the same?
externalPort := binary.BigEndian.Uint16(resp[42:44])
externalIPBytes := [16]byte{}
copy(externalIPBytes[:], resp[44:])
externalIP := netaddr.IPFrom16(externalIPBytes)
external := netaddr.IPPortFrom(externalIP, externalPort)
lifetime := time.Second * time.Duration(res.Lifetime)
now := time.Now()
mapping := &pcpMapping{
external: external,
renewAfter: now.Add(lifetime / 2),
goodUntil: now.Add(lifetime),
}
return mapping, nil
}
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode uint8
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = b[3]
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}

View File

@@ -0,0 +1,62 @@
// 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 portmapper
import (
"encoding/binary"
"testing"
"inet.af/netaddr"
)
var examplePCPMapResponse = []byte{2, 129, 0, 0, 0, 0, 28, 32, 0, 2, 155, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 112, 9, 24, 241, 208, 251, 45, 157, 76, 10, 188, 17, 0, 0, 0, 4, 210, 4, 210, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 135, 180, 175, 246}
func TestParsePCPMapResponse(t *testing.T) {
mapping, err := parsePCPMapResponse(examplePCPMapResponse)
if err != nil {
t.Fatalf("failed to parse PCP Map Response: %v", err)
}
if mapping == nil {
t.Fatalf("got nil mapping when expected non-nil")
}
expectedAddr := netaddr.MustParseIPPort("135.180.175.246:1234")
if mapping.external != expectedAddr {
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
}
}
const (
serverResponseBit = 1 << 7
fakeLifetimeSec = 1<<31 - 1
)
func buildPCPDiscoResponse(req []byte) []byte {
out := make([]byte, 24)
out[0] = pcpVersion
out[1] = req[1] | serverResponseBit
out[3] = 0
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
return out
}
func buildPCPMapResponse(req []byte) []byte {
out := make([]byte, 24+36)
out[0] = pcpVersion
out[1] = req[1] | serverResponseBit
out[3] = 0
binary.BigEndian.PutUint32(out[4:8], 1<<30)
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
mapResp := out[24:]
mapReq := req[24:]
// copy nonce, protocol and internal port
copy(mapResp[:13], mapReq[:13])
copy(mapResp[16:18], mapReq[16:18])
// assign external port
binary.BigEndian.PutUint16(mapResp[18:20], 4242)
assignedIP := netaddr.IPv4(127, 0, 0, 1)
assignedIP16 := assignedIP.As16()
copy(mapResp[20:36], assignedIP16[:])
return out
}

View File

@@ -3,30 +3,42 @@
// license that can be found in the LICENSE file.
// Package portmapper is a UDP port mapping client. It currently allows for mapping over
// NAT-PMP and UPnP, but will perhaps do PCP later.
// NAT-PMP, UPnP, and PCP.
package portmapper
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"sync"
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
// Debug knobs for "tailscaled debug --portmap".
var (
VerboseLogs bool
// Disable* disables a specific service from mapping.
DisableUPnP bool
DisablePMP bool
DisablePCP bool
)
// References:
//
// NAT-PMP: https://tools.ietf.org/html/rfc6886
// PCP: https://tools.ietf.org/html/rfc6887
// portMapServiceTimeout is the time we wait for port mapping
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
@@ -44,6 +56,8 @@ type Client struct {
logf logger.Logf
ipAndGateway func() (gw, ip netaddr.IP, ok bool)
onChange func() // or nil
testPxPPort uint16 // if non-zero, pxpPort to use for tests
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
mu sync.Mutex // guards following, and all fields thereof
@@ -62,8 +76,11 @@ type Client struct {
pmpPubIPTime time.Time // time pmpPubIP last verified
pmpLastEpoch uint32
pcpSawTime time.Time // time we last saw PCP was available
uPnPSawTime time.Time // time we last saw UPnP was available
pcpSawTime time.Time // time we last saw PCP was available
uPnPSawTime time.Time // time we last saw UPnP was available
uPnPMeta uPnPDiscoResponse // Location header from UPnP UDP discovery response
uPnPHTTPClient *http.Client // netns-configured HTTP client for UPnP; nil until needed
localPort uint16
@@ -99,7 +116,8 @@ func (c *Client) HaveMapping() bool {
//
// All fields are immutable once created.
type pmpMapping struct {
gw netaddr.IP
c *Client
gw netaddr.IPPort
external netaddr.IPPort
internal netaddr.IPPort
renewAfter time.Time // the time at which we want to renew the mapping
@@ -118,13 +136,13 @@ func (p *pmpMapping) External() netaddr.IPPort { return p.external }
// Release does a best effort fire-and-forget release of the PMP mapping m.
func (m *pmpMapping) Release(ctx context.Context) {
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
uc, err := m.c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
uc.WriteTo(pkt, m.gw.UDPAddr())
}
// NewClient returns a new portmapping client.
@@ -199,6 +217,44 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
return
}
// pxpPort returns the NAT-PMP and PCP port number.
// It returns 5351, except for in tests where it varies by run.
func (c *Client) pxpPort() uint16 {
if c.testPxPPort != 0 {
return c.testPxPPort
}
return pmpDefaultPort
}
// upnpPort returns the UPnP discovery port number.
// It returns 1900, except for in tests where it varies by run.
func (c *Client) upnpPort() uint16 {
if c.testUPnPPort != 0 {
return c.testUPnPPort
}
return upnpDefaultPort
}
func (c *Client) listenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) {
// When running under testing conditions, we bind the IGD server
// to localhost, and may be running in an environment where our
// netns code would decide that binding the portmapper client
// socket to the default route interface is the correct way to
// ensure connectivity. This can result in us trying to send
// packets for 127.0.0.1 out the machine's LAN interface, which
// obviously gets dropped on the floor.
//
// So, under those testing conditions, do _not_ use netns to
// create listening sockets. Such sockets are vulnerable to
// routing loops, but it's tests that don't set up routing loops,
// so we don't care.
if c.testPxPPort != 0 || c.testUPnPPort != 0 || os.Getenv("GITHUB_ACTIONS") == "true" {
var lc net.ListenConfig
return lc.ListenPacket(ctx, network, addr)
}
return netns.Listener().ListenPacket(ctx, network, addr)
}
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
if c.mapping != nil {
if releaseOld {
@@ -210,6 +266,7 @@ func (c *Client) invalidateMappingsLocked(releaseOld bool) {
c.pmpPubIPTime = time.Time{}
c.pcpSawTime = time.Time{}
c.uPnPSawTime = time.Time{}
c.uPnPMeta = uPnPDiscoResponse{}
}
func (c *Client) sawPMPRecently() bool {
@@ -225,6 +282,10 @@ func (c *Client) sawPMPRecentlyLocked() bool {
func (c *Client) sawPCPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.sawPCPRecentlyLocked()
}
func (c *Client) sawPCPRecentlyLocked() bool {
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
@@ -323,12 +384,18 @@ func (c *Client) createMapping() {
}
}
// wildcardIP is used when the previous external IP is not known for PCP port mapping.
var wildcardIP = netaddr.MustParseIP("0.0.0.0")
// createOrGetMapping either creates a new mapping or returns a cached
// valid one.
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPort, err error) {
if DisableUPnP && DisablePCP && DisablePMP {
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return netaddr.IPPort{}, NoMappingError{ErrGatewayRange}
@@ -337,10 +404,6 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
c.mu.Lock()
localPort := c.localPort
internalAddr := netaddr.IPPortFrom(myIP, localPort)
m := &pmpMapping{
gw: gw,
internal: internalAddr,
}
// prevPort is the port we had most previously, if any. We try
// to ask for the same port. 0 means to give us any port.
@@ -357,25 +420,45 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
prevPort = m.External().Port()
}
// If we just did a Probe (e.g. via netchecker) but didn't
// find a PMP service, bail out early rather than probing
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
if haveRecentPMP {
m.external = m.external.WithIP(c.pmpPubIP)
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
if DisablePCP && DisablePMP {
c.mu.Unlock()
// fallback to UPnP portmapping
if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return mapping, nil
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return external, nil
}
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
// If we just did a Probe (e.g. via netchecker) but didn't
// find a PMP service, bail out early rather than probing
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
haveRecentPCP := c.sawPCPRecentlyLocked()
// Since PMP mapping may require multiple calls, and it's not clear from the outset
// whether we're doing a PCP or PMP call, initialize the PMP mapping here,
// and only return it once completed.
//
// PCP returns all the information necessary for a mapping in a single packet, so we can
// construct it upon receiving that packet.
m := &pmpMapping{
c: c,
gw: netaddr.IPPortFrom(gw, c.pxpPort()),
internal: internalAddr,
}
if haveRecentPMP {
m.external = m.external.WithIP(c.pmpPubIP)
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP && !haveRecentPCP {
c.mu.Unlock()
// fallback to UPnP portmapping
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return external, nil
}
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
c.mu.Unlock()
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
uc, err := c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return netaddr.IPPort{}, err
}
@@ -384,20 +467,31 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
defer closeCloserOnContextDone(ctx, uc)()
pmpAddr := netaddr.IPPortFrom(gw, pmpPort)
pmpAddru := pmpAddr.UDPAddr()
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort())
pxpAddru := pxpAddr.UDPAddr()
// Ask for our external address if needed.
if m.external.IP().IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
preferPCP := !DisablePCP && (DisablePMP || (!haveRecentPMP && haveRecentPCP))
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
if preferPCP {
// TODO replace wildcardIP here with previous external if known.
// Only do PCP mapping in the case when PMP did not appear to be available recently.
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP)
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
return netaddr.IPPort{}, err
}
}
} else {
// Ask for our external address if needed.
if m.external.IP().IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pxpAddru); err != nil {
return netaddr.IPPort{}, err
}
}
// And ask for a mapping.
pmpReqMapping := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteTo(pmpReqMapping, pmpAddru); err != nil {
return netaddr.IPPort{}, err
pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
return netaddr.IPPort{}, err
}
}
res := make([]byte, 1500)
@@ -418,25 +512,46 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
if !ok {
continue
}
if src == pmpAddr {
pres, ok := parsePMPResponse(res[:n])
if !ok {
c.logf("unexpected PMP response: % 02x", res[:n])
continue
}
if pres.ResultCode != 0 {
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external = m.external.WithIP(pres.PublicAddr)
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external = m.external.WithPort(pres.ExternalPort)
d := time.Duration(pres.MappingValidSeconds) * time.Second
now := time.Now()
m.goodUntil = now.Add(d)
m.renewAfter = now.Add(d / 2) // renew in half the time
m.epoch = pres.SecondsSinceEpoch
if src == pxpAddr {
version := res[0]
switch version {
case pmpVersion:
pres, ok := parsePMPResponse(res[:n])
if !ok {
c.logf("unexpected PMP response: % 02x", res[:n])
continue
}
if pres.ResultCode != 0 {
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external = m.external.WithIP(pres.PublicAddr)
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external = m.external.WithPort(pres.ExternalPort)
d := time.Duration(pres.MappingValidSeconds) * time.Second
now := time.Now()
m.goodUntil = now.Add(d)
m.renewAfter = now.Add(d / 2) // renew in half the time
m.epoch = pres.SecondsSinceEpoch
}
case pcpVersion:
pcpMapping, err := parsePCPMapResponse(res[:n])
if err != nil {
c.logf("failed to get PCP mapping: %v", err)
// PCP should only have a single packet response
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
pcpMapping.c = c
pcpMapping.internal = m.internal
pcpMapping.gw = netaddr.IPPortFrom(gw, c.pxpPort())
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = pcpMapping
return pcpMapping.external, nil
default:
c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
}
@@ -453,10 +568,11 @@ type pmpResultCode uint16
// NAT-PMP constants.
const (
pmpPort = 5351
pmpDefaultPort = 5351
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
pmpVersion = 0
pmpOpMapPublicAddr = 0
pmpOpMapUDP = 1
pmpOpReply = 0x80 // OR'd into request's op code on response
@@ -550,7 +666,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
}
}()
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
uc, err := c.listenPacket(context.Background(), "udp4", ":0")
if err != nil {
c.logf("ProbePCP: %v", err)
return res, err
@@ -560,46 +676,32 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
defer cancel()
defer closeCloserOnContextDone(ctx, uc)()
if c.sawUPnPRecently() {
res.UPnP = true
} else {
hasUPnP := make(chan bool, 1)
defer func() {
res.UPnP = <-hasUPnP
}()
go func() {
client, err := getUPnPClient(ctx, gw)
if err == nil && client != nil {
hasUPnP <- true
c.mu.Lock()
c.uPnPSawTime = time.Now()
c.mu.Unlock()
}
close(hasUPnP)
}()
}
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort()).UDPAddr()
upnpAddr := netaddr.IPPortFrom(gw, c.upnpPort()).UDPAddr()
// Don't send probes to services that we recently learned (for
// the same gw/myIP) are available. See
// https://github.com/tailscale/tailscale/issues/1001
if c.sawPMPRecently() {
res.PMP = true
} else {
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
} else if !DisablePMP {
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else {
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
} else if !DisablePCP {
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
}
if c.sawUPnPRecently() {
res.UPnP = true
} else if !DisableUPnP {
uc.WriteTo(uPnPPacket, upnpAddr)
}
buf := make([]byte, 1500)
pcpHeard := false // true when we get any PCP response
for {
if pcpHeard && res.PMP {
if pcpHeard && res.PMP && res.UPnP {
// Nothing more to discover.
return res, nil
}
@@ -610,9 +712,27 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
}
return res, err
}
port := addr.(*net.UDPAddr).Port
port := uint16(addr.(*net.UDPAddr).Port)
switch port {
case pcpPort: // same as pmpPort
case c.upnpPort():
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
meta, err := parseUPnPDiscoResponse(buf[:n])
if err != nil {
c.logf("unrecognized UPnP discovery response; ignoring")
}
if VerboseLogs {
c.logf("UPnP reply %+v, %q", meta, buf[:n])
}
res.UPnP = true
c.mu.Lock()
c.uPnPSawTime = time.Now()
if c.uPnPMeta != meta {
c.logf("UPnP meta changed: %+v", meta)
c.uPnPMeta = meta
}
c.mu.Unlock()
}
case c.pxpPort(): // same value for PMP and PCP
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
pcpHeard = true
@@ -652,75 +772,15 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
}
}
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
const (
pcpVersion = 2
pcpPort = 5351
pcpCodeOK = 0
pcpCodeNotAuthorized = 2
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
)
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion // version
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
// pcpMapRequest generates a PCP packet with a MAP opcode.
func pcpMapRequest(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
if delete {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec := netaddr.MustParseIP("0.0.0.0")
v4unspec16 := v4unspec.As16()
copy(mapOp[20:], v4unspec16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode uint8
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = b[3]
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
// uPnPPacket is the UPnP UDP discovery packet's request body.
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")

View File

@@ -7,6 +7,7 @@ package portmapper
import (
"context"
"os"
"reflect"
"strconv"
"testing"
"time"
@@ -55,3 +56,70 @@ func TestClientProbeThenMap(t *testing.T) {
ext, err := c.createOrGetMapping(context.Background())
t.Logf("createOrGetMapping: %v, %v", ext, err)
}
func TestProbeIntegration(t *testing.T) {
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: true, PCP: true, UPnP: true})
if err != nil {
t.Fatal(err)
}
defer igd.Close()
c := newTestClient(t, igd)
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
defer c.Close()
res, err := c.Probe(context.Background())
if err != nil {
t.Fatalf("Probe: %v", err)
}
if !res.UPnP {
t.Errorf("didn't detect UPnP")
}
st := igd.stats()
want := igdCounters{
numUPnPDiscoRecv: 1,
numPMPRecv: 1,
numPCPRecv: 1,
numPCPDiscoRecv: 1,
numPMPPublicAddrRecv: 1,
}
if !reflect.DeepEqual(st, want) {
t.Errorf("unexpected stats:\n got: %+v\nwant: %+v", st, want)
}
t.Logf("Probe: %+v", res)
t.Logf("IGD stats: %+v", st)
// TODO(bradfitz): finish
}
func TestPCPIntegration(t *testing.T) {
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: false, PCP: true, UPnP: false})
if err != nil {
t.Fatal(err)
}
defer igd.Close()
c := newTestClient(t, igd)
defer c.Close()
res, err := c.Probe(context.Background())
if err != nil {
t.Fatalf("probe failed: %v", err)
}
if res.UPnP || res.PMP {
t.Errorf("probe unexpectedly saw upnp or pmp: %+v", res)
}
if !res.PCP {
t.Fatalf("probe did not see pcp: %+v", res)
}
external, err := c.createOrGetMapping(context.Background())
if err != nil {
t.Fatalf("failed to get mapping: %v", err)
}
if external.IsZero() {
t.Errorf("got zero IP, expected non-zero")
}
if c.mapping == nil {
t.Errorf("got nil mapping after successful createOrGetMapping")
}
}

View File

@@ -2,21 +2,30 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios
// +build !ios
// (https://github.com/tailscale/tailscale/issues/2495)
package portmapper
import (
"bufio"
"bytes"
"context"
"fmt"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/dcps/internetgateway2"
"inet.af/netaddr"
"tailscale.com/control/controlknobs"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
// References:
@@ -44,7 +53,8 @@ func (u *upnpMapping) Release(ctx context.Context) {
}
// upnpClient is an interface over the multiple different clients exported by goupnp,
// exposing the functions we need for portmapping. They are auto-generated from XML-specs.
// exposing the functions we need for portmapping. Those clients are auto-generated from XML-specs,
// which is why they're not very idiomatic.
type upnpClient interface {
AddPortMapping(
ctx context.Context,
@@ -77,7 +87,7 @@ type upnpClient interface {
// greater than 0. From the spec, it appears if it is set to 0, it will switch to using
// 604800 seconds, but not sure why this is desired. The recommended time is 3600 seconds.
leaseDurationSec uint32,
) (err error)
) error
DeletePortMapping(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddress(ctx context.Context) (externalIPAddress string, err error)
@@ -92,6 +102,8 @@ const tsPortMappingDesc = "tailscale-portmap"
// behavior of calling AddPortMapping with port = 0 to specify a wildcard port.
// It returns the new external port (which may not be identical to the external port specified),
// or an error.
//
// TODO(bradfitz): also returned the actual lease duration obtained. and check it regularly.
func addAnyPortMapping(
ctx context.Context,
upnp upnpClient,
@@ -130,51 +142,89 @@ func addAnyPortMapping(
return externalPort, err
}
// getUPnPClients gets a client for interfacing with UPnP, ignoring the underlying protocol for
// getUPnPClient gets a client for interfacing with UPnP, ignoring the underlying protocol for
// now.
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
func getUPnPClient(ctx context.Context, gw netaddr.IP) (upnpClient, error) {
if controlknobs.DisableUPnP() {
//
// The gw is the detected gateway.
//
// The meta is the most recently parsed UDP discovery packet response
// from the Internet Gateway Device.
//
// The provided ctx is not retained in the returned upnpClient, but
// its associated HTTP client is (if set via goupnp.WithHTTPClient).
func getUPnPClient(ctx context.Context, logf logger.Logf, gw netaddr.IP, meta uPnPDiscoResponse) (client upnpClient, err error) {
if controlknobs.DisableUPnP() || DisableUPnP {
return nil, nil
}
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
// Attempt to connect over the multiple available connection types concurrently,
// returning the fastest.
// TODO(jknodt): this url seems super brittle? maybe discovery is better but this is faster
u, err := url.Parse(fmt.Sprintf("http://%s:5000/rootDesc.xml", gw))
if meta.Location == "" {
return nil, nil
}
if VerboseLogs {
logf("fetching %v", meta.Location)
}
u, err := url.Parse(meta.Location)
if err != nil {
return nil, err
}
clients := make(chan upnpClient, 3)
go func() {
var err error
ip1Clients, err := internetgateway2.NewWANIPConnection1ClientsByURL(ctx, u)
if err == nil && len(ip1Clients) > 0 {
clients <- ip1Clients[0]
}
}()
go func() {
ip2Clients, err := internetgateway2.NewWANIPConnection2ClientsByURL(ctx, u)
if err == nil && len(ip2Clients) > 0 {
clients <- ip2Clients[0]
}
}()
go func() {
ppp1Clients, err := internetgateway2.NewWANPPPConnection1ClientsByURL(ctx, u)
if err == nil && len(ppp1Clients) > 0 {
clients <- ppp1Clients[0]
ipp, err := netaddr.ParseIPPort(u.Host)
if err != nil {
return nil, fmt.Errorf("unexpected host %q in %q", u.Host, meta.Location)
}
if ipp.IP() != gw {
return nil, fmt.Errorf("UPnP discovered root %q does not match gateway IP %v; ignoring UPnP",
meta.Location, gw)
}
// We're fetching a smallish XML document over plain HTTP
// across the local LAN, without using DNS. There should be
// very few round trips and low latency, so one second is a
// long time.
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// This part does a network fetch.
root, err := goupnp.DeviceByURL(ctx, u)
if err != nil {
return nil, err
}
defer func() {
if client == nil {
return
}
logf("saw UPnP type %v at %v; %v (%v)",
strings.TrimPrefix(fmt.Sprintf("%T", client), "*internetgateway2."),
meta.Location, root.Device.FriendlyName, root.Device.Manufacturer)
}()
select {
case client := <-clients:
return client, nil
case <-ctx.Done():
return nil, ctx.Err()
// These parts don't do a network fetch.
// Pick the best service type available.
if cc, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
return cc[0], nil
}
if cc, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
return cc[0], nil
}
if cc, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
return cc[0], nil
}
return nil, nil
}
func (c *Client) upnpHTTPClientLocked() *http.Client {
if c.uPnPHTTPClient == nil {
c.uPnPHTTPClient = &http.Client{
Transport: &http.Transport{
DialContext: netns.NewDialer().DialContext,
IdleConnTimeout: 2 * time.Second, // LAN is cheap
},
}
}
return c.uPnPHTTPClient
}
// getUPnPPortMapping attempts to create a port-mapping over the UPnP protocol. On success,
@@ -186,7 +236,7 @@ func (c *Client) getUPnPPortMapping(
internal netaddr.IPPort,
prevPort uint16,
) (external netaddr.IPPort, ok bool) {
if controlknobs.DisableUPnP() {
if controlknobs.DisableUPnP() || DisableUPnP {
return netaddr.IPPort{}, false
}
now := time.Now()
@@ -199,11 +249,17 @@ func (c *Client) getUPnPPortMapping(
var err error
c.mu.Lock()
oldMapping, ok := c.mapping.(*upnpMapping)
meta := c.uPnPMeta
httpClient := c.upnpHTTPClientLocked()
c.mu.Unlock()
if ok && oldMapping != nil {
client = oldMapping.client
} else {
client, err = getUPnPClient(ctx, gw)
ctx := goupnp.WithHTTPClient(ctx, httpClient)
client, err = getUPnPClient(ctx, c.logf, gw, meta)
if VerboseLogs {
c.logf("getUPnPClient: %T, %v", client, err)
}
if err != nil {
return netaddr.IPPort{}, false
}
@@ -221,11 +277,17 @@ func (c *Client) getUPnPPortMapping(
internal.IP().String(),
time.Second*pmpMapLifetimeSec,
)
if VerboseLogs {
c.logf("addAnyPortMapping: %v, %v", newPort, err)
}
if err != nil {
return netaddr.IPPort{}, false
}
// TODO cache this ip somewhere?
extIP, err := client.GetExternalIPAddress(ctx)
if VerboseLogs {
c.logf("client.GetExternalIPAddress: %v, %v", extIP, err)
}
if err != nil {
// TODO this doesn't seem right
return netaddr.IPPort{}, false
@@ -246,3 +308,25 @@ func (c *Client) getUPnPPortMapping(
c.localPort = newPort
return upnp.external, true
}
type uPnPDiscoResponse struct {
Location string
// Server describes what version the UPnP is, such as MiniUPnPd/2.x.x
Server string
// USN is the serial number of the device, which also contains
// what kind of UPnP service is being offered, i.e. InternetGatewayDevice:2
USN string
}
// parseUPnPDiscoResponse parses a UPnP HTTP-over-UDP discovery response.
func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
var r uPnPDiscoResponse
res, err := http.ReadResponse(bufio.NewReaderSize(bytes.NewReader(body), 128), nil)
if err != nil {
return r, err
}
r.Location = res.Header.Get("Location")
r.Server = res.Header.Get("Server")
r.USN = res.Header.Get("Usn")
return r, nil
}

121
net/portmapper/upnp_test.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portmapper
import (
"bytes"
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"testing"
"inet.af/netaddr"
)
// Google Wifi
const (
googleWifiUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:2\r\nUSN: uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2\r\nEXT:\r\nSERVER: Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9\r\nLOCATION: http://192.168.86.1:5000/rootDesc.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1\r\nBOOTID.UPNP.ORG: 1\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n"
googleWifiRootDescXML = `<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0"><specVersion><major>1</major><minor>0</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:2</deviceType><friendlyName>OnHub</friendlyName><manufacturer>Google</manufacturer><manufacturerURL>http://google.com/</manufacturerURL><modelDescription>Wireless Router</modelDescription><modelName>OnHub</modelName><modelNumber>1</modelNumber><modelURL>https://on.google.com/hub/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece</UDN><serviceList><service><serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType><serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId><controlURL>/ctl/L3F</controlURL><eventSubURL>/evt/L3F</eventSubURL><SCPDURL>/L3F.xml</SCPDURL></service><service><serviceType>urn:schemas-upnp-org:service:DeviceProtection:1</serviceType><serviceId>urn:upnp-org:serviceId:DeviceProtection1</serviceId><controlURL>/ctl/DP</controlURL><eventSubURL>/evt/DP</eventSubURL><SCPDURL>/DP.xml</SCPDURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANDevice:2</deviceType><friendlyName>WANDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>WAN Device</modelDescription><modelName>WAN Device</modelName><modelNumber>20210414</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ecf</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType><serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId><controlURL>/ctl/CmnIfCfg</controlURL><eventSubURL>/evt/CmnIfCfg</eventSubURL><SCPDURL>/WANCfg.xml</SCPDURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:2</deviceType><friendlyName>WANConnectionDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>MiniUPnP daemon</modelDescription><modelName>MiniUPnPd</modelName><modelNumber>20210414</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ec0</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANIPConnection:2</serviceType><serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId><controlURL>/ctl/IPConn</controlURL><eventSubURL>/evt/IPConn</eventSubURL><SCPDURL>/WANIPCn.xml</SCPDURL></service></serviceList></device></deviceList></device></deviceList><presentationURL>http://testwifi.here/</presentationURL></device></root>`
)
// pfSense 2.5.0-RELEASE / FreeBSD 12.2-STABLE
const (
pfSenseUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: http://192.168.1.1:2189/rootDesc.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n"
pfSenseRootDescXML = `<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="1337"><specVersion><major>1</major><minor>1</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType><friendlyName>FreeBSD router</friendlyName><manufacturer>FreeBSD</manufacturer><manufacturerURL>http://www.freebsd.org/</manufacturerURL><modelDescription>FreeBSD router</modelDescription><modelName>FreeBSD router</modelName><modelNumber>2.5.0-RELEASE</modelNumber><modelURL>http://www.freebsd.org/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac11</UDN><serviceList><service><serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType><serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId><SCPDURL>/L3F.xml</SCPDURL><controlURL>/ctl/L3F</controlURL><eventSubURL>/evt/L3F</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType><friendlyName>WANDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>WAN Device</modelDescription><modelName>WAN Device</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac12</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType><serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId><SCPDURL>/WANCfg.xml</SCPDURL><controlURL>/ctl/CmnIfCfg</controlURL><eventSubURL>/evt/CmnIfCfg</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType><friendlyName>WANConnectionDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>MiniUPnP daemon</modelDescription><modelName>MiniUPnPd</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac13</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType><serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId><SCPDURL>/WANIPCn.xml</SCPDURL><controlURL>/ctl/IPConn</controlURL><eventSubURL>/evt/IPConn</eventSubURL></service></serviceList></device></deviceList></device></deviceList><presentationURL>https://192.168.1.1/</presentationURL></device></root>`
)
func TestParseUPnPDiscoResponse(t *testing.T) {
tests := []struct {
name string
headers string
want uPnPDiscoResponse
}{
{"google", googleWifiUPnPDisco, uPnPDiscoResponse{
Location: "http://192.168.86.1:5000/rootDesc.xml",
Server: "Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9",
USN: "uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
}},
{"pfsense", pfSenseUPnPDisco, uPnPDiscoResponse{
Location: "http://192.168.1.1:2189/rootDesc.xml",
Server: "FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1",
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseUPnPDiscoResponse([]byte(tt.headers))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unexpected result:\n got: %+v\nwant: %+v\n", got, tt.want)
}
})
}
}
func TestGetUPnPClient(t *testing.T) {
tests := []struct {
name string
xmlBody string
want string
wantLog string
}{
{
"google",
googleWifiRootDescXML,
"*internetgateway2.WANIPConnection2",
"saw UPnP type WANIPConnection2 at http://127.0.0.1:NNN/rootDesc.xml; OnHub (Google)\n",
},
{
"pfsense",
pfSenseRootDescXML,
"*internetgateway2.WANIPConnection1",
"saw UPnP type WANIPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; FreeBSD router (FreeBSD)\n",
},
// TODO(bradfitz): find a PPP one in the wild
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/rootDesc.xml" {
io.WriteString(w, tt.xmlBody)
return
}
http.NotFound(w, r)
}))
defer ts.Close()
gw, _ := netaddr.FromStdIP(ts.Listener.Addr().(*net.TCPAddr).IP)
var logBuf bytes.Buffer
logf := func(format string, a ...interface{}) {
fmt.Fprintf(&logBuf, format, a...)
logBuf.WriteByte('\n')
}
c, err := getUPnPClient(context.Background(), logf, gw, uPnPDiscoResponse{
Location: ts.URL + "/rootDesc.xml",
})
if err != nil {
t.Fatal(err)
}
got := fmt.Sprintf("%T", c)
if got != tt.want {
t.Errorf("got %v; want %v", got, tt.want)
}
gotLog := regexp.MustCompile(`127\.0\.0\.1:\d+`).ReplaceAllString(logBuf.String(), "127.0.0.1:NNN")
if gotLog != tt.wantLog {
t.Errorf("logged %q; want %q", gotLog, tt.wantLog)
}
})
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gofuzz
// +build gofuzz
package stun

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build tailscale_go
// +build tailscale_go
// We want to use https://github.com/golang/go/issues/41048 but it's only in the

View File

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

365
net/tstun/tap_linux.go Normal file
View File

@@ -0,0 +1,365 @@
// 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 tstun
import (
"fmt"
"net"
"os"
"os/exec"
"github.com/insomniacslk/dhcp/dhcpv4"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/tun"
"inet.af/netaddr"
"inet.af/netstack/tcpip"
"inet.af/netstack/tcpip/buffer"
"inet.af/netstack/tcpip/header"
"inet.af/netstack/tcpip/network/ipv4"
"inet.af/netstack/tcpip/transport/udp"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
)
// TODO: this was randomly generated once. Maybe do it per process start? But
// then an upgraded tailscaled would be visible to devices behind it. So
// maybe instead make it a function of the tailscaled's wireguard public key?
// For now just hard code it.
var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93}
func init() { createTAP = createTAPLinux }
func createTAPLinux(tapName, bridgeName string) (tun.Device, error) {
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
if err != nil {
return nil, err
}
dev, err := openDevice(fd, tapName, bridgeName)
if err != nil {
unix.Close(fd)
return nil, err
}
return dev, nil
}
func openDevice(fd int, tapName, bridgeName string) (tun.Device, error) {
ifr, err := unix.NewIfreq(tapName)
if err != nil {
return nil, err
}
// Flags are stored as a uint16 in the ifreq union.
ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI)
if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil {
return nil, err
}
if err := run("ip", "link", "set", "dev", tapName, "up"); err != nil {
return nil, err
}
if bridgeName != "" {
if err := run("brctl", "addif", bridgeName, tapName); err != nil {
return nil, err
}
}
// Also sets non-blocking I/O on fd when creating tun.Device.
dev, _, err := tun.CreateUnmonitoredTUNFromFD(fd) // TODO: MTU
if err != nil {
return nil, err
}
return dev, nil
}
type etherType [2]byte
var (
etherTypeARP = etherType{0x08, 0x06}
etherTypeIPv4 = etherType{0x08, 0x00}
etherTypeIPv6 = etherType{0x86, 0xDD}
)
const ipv4HeaderLen = 20
const (
consumePacket = true
passOnPacket = false
)
// handleTAPFrame handles receiving a raw TAP ethernet frame and reports whether
// it's been handled (that is, whether it should NOT be passed to wireguard).
func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool {
if len(ethBuf) < ethernetFrameSize {
// Corrupt. Ignore.
if tapDebug {
t.logf("tap: short TAP frame")
}
return consumePacket
}
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
_ = ethDstMAC
et := etherType{ethBuf[12], ethBuf[13]}
switch et {
default:
if tapDebug {
t.logf("tap: ignoring etherType %v", et)
}
return consumePacket // filter out packet we should ignore
case etherTypeIPv6:
// TODO: support DHCPv6/ND/etc later. For now pass all to WireGuard.
if tapDebug {
t.logf("tap: ignoring IPv6 %v", et)
}
return passOnPacket
case etherTypeIPv4:
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen {
// Bogus IPv4. Eat.
if tapDebug {
t.logf("tap: short ipv4")
}
return consumePacket
}
return t.handleDHCPRequest(ethBuf)
case etherTypeARP:
arpPacket := header.ARP(ethBuf[ethernetFrameSize:])
if !arpPacket.IsValid() {
// Bogus ARP. Eat.
return consumePacket
}
switch arpPacket.Op() {
case header.ARPRequest:
req := arpPacket // better name at this point
buf := make([]byte, header.EthernetMinimumSize+header.ARPSize)
// Our ARP "Table" of one:
var srcMAC [6]byte
copy(srcMAC[:], ethSrcMAC)
if old := t.destMAC(); old != srcMAC {
t.destMACAtomic.Store(srcMAC)
}
eth := header.Ethernet(buf)
eth.Encode(&header.EthernetFields{
SrcAddr: tcpip.LinkAddress(ourMAC[:]),
DstAddr: tcpip.LinkAddress(ethSrcMAC),
Type: 0x0806, // arp
})
res := header.ARP(buf[header.EthernetMinimumSize:])
res.SetIPv4OverEthernet()
res.SetOp(header.ARPReply)
// If the client's asking about their own IP, tell them it's
// their own MAC. TODO(bradfitz): remove String allocs.
if net.IP(req.ProtocolAddressTarget()).String() == theClientIP {
copy(res.HardwareAddressSender(), ethSrcMAC)
} else {
copy(res.HardwareAddressSender(), ourMAC[:])
}
copy(res.ProtocolAddressSender(), req.ProtocolAddressTarget())
copy(res.HardwareAddressTarget(), req.HardwareAddressSender())
copy(res.ProtocolAddressTarget(), req.ProtocolAddressSender())
n, err := t.tdev.Write(buf, 0)
if tapDebug {
t.logf("tap: wrote ARP reply %v, %v", n, err)
}
}
return consumePacket
}
}
// TODO(bradfitz): remove these hard-coded values and move from a /24 to a /10 CGNAT as the range.
const theClientIP = "100.70.145.3" // TODO: make dynamic from netmap
const routerIP = "100.70.145.1" // must be in same netmask (currently hack at /24) as theClientIP
// handleDHCPRequest handles receiving a raw TAP ethernet frame and reports whether
// it's been handled as a DHCP request. That is, it reports whether the frame should
// be ignored by the caller and not passed on.
func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
const udpHeader = 8
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen+udpHeader {
if tapDebug {
t.logf("tap: DHCP short")
}
return passOnPacket
}
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
if string(ethDstMAC) != "\xff\xff\xff\xff\xff\xff" {
// Not a broadcast
if tapDebug {
t.logf("tap: dhcp no broadcast")
}
return passOnPacket
}
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(ethBuf[ethernetFrameSize:])
if p.IPProto != ipproto.UDP || p.Src.Port() != 68 || p.Dst.Port() != 67 {
// Not a DHCP request.
if tapDebug {
t.logf("tap: DHCP wrong meta")
}
return passOnPacket
}
dp, err := dhcpv4.FromBytes(ethBuf[ethernetFrameSize+ipv4HeaderLen+udpHeader:])
if err != nil {
// Bogus. Trash it.
if tapDebug {
t.logf("tap: DHCP FromBytes bad")
}
return consumePacket
}
if tapDebug {
t.logf("tap: DHCP request: %+v", dp)
}
switch dp.MessageType() {
case dhcpv4.MessageTypeDiscover:
offer, err := dhcpv4.New(
dhcpv4.WithReply(dp),
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route
dhcpv4.WithDNS(net.ParseIP("100.100.100.100")),
dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))),
dhcpv4.WithYourIP(net.ParseIP(theClientIP)),
dhcpv4.WithLeaseTime(3600), // hour works
//dhcpv4.WithHwAddr(ethSrcMAC),
dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())), // TODO: wrong
//dhcpv4.WithTransactionID(dp.TransactionID),
)
if err != nil {
t.logf("error building DHCP offer: %v", err)
return consumePacket
}
// Make a layer 2 packet to write out:
pkt := packLayer2UDP(
offer.ToBytes(),
ourMAC, ethSrcMAC,
netaddr.IPPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
netaddr.IPPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
)
n, err := t.tdev.Write(pkt, 0)
if tapDebug {
t.logf("tap: wrote DHCP OFFER %v, %v", n, err)
}
case dhcpv4.MessageTypeRequest:
ack, err := dhcpv4.New(
dhcpv4.WithReply(dp),
dhcpv4.WithMessageType(dhcpv4.MessageTypeAck),
dhcpv4.WithDNS(net.ParseIP("100.100.100.100")),
dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route
dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))),
dhcpv4.WithYourIP(net.ParseIP(theClientIP)), // Hello world
dhcpv4.WithLeaseTime(3600), // hour works
dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())),
)
if err != nil {
t.logf("error building DHCP ack: %v", err)
return consumePacket
}
// Make a layer 2 packet to write out:
pkt := packLayer2UDP(
ack.ToBytes(),
ourMAC, ethSrcMAC,
netaddr.IPPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
netaddr.IPPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
)
n, err := t.tdev.Write(pkt, 0)
if tapDebug {
t.logf("tap: wrote DHCP ACK %v, %v", n, err)
}
default:
if tapDebug {
t.logf("tap: unknown DHCP type")
}
}
return consumePacket
}
func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netaddr.IPPort) []byte {
buf := buffer.NewView(header.EthernetMinimumSize + header.UDPMinimumSize + header.IPv4MinimumSize + len(payload))
payloadStart := len(buf) - len(payload)
copy(buf[payloadStart:], payload)
srcB := src.IP().As4()
srcIP := tcpip.Address(srcB[:])
dstB := dst.IP().As4()
dstIP := tcpip.Address(dstB[:])
// Ethernet header
eth := header.Ethernet(buf)
eth.Encode(&header.EthernetFields{
SrcAddr: tcpip.LinkAddress(srcMAC),
DstAddr: tcpip.LinkAddress(dstMAC),
Type: ipv4.ProtocolNumber,
})
// IP header
ipbuf := buf[header.EthernetMinimumSize:]
ip := header.IPv4(ipbuf)
ip.Encode(&header.IPv4Fields{
TotalLength: uint16(len(ipbuf)),
TTL: 65,
Protocol: uint8(udp.ProtocolNumber),
SrcAddr: srcIP,
DstAddr: dstIP,
})
ip.SetChecksum(^ip.CalculateChecksum())
// UDP header
u := header.UDP(buf[header.EthernetMinimumSize+header.IPv4MinimumSize:])
u.Encode(&header.UDPFields{
SrcPort: src.Port(),
DstPort: dst.Port(),
Length: uint16(header.UDPMinimumSize + len(payload)),
})
// Calculate the UDP pseudo-header checksum.
xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, srcIP, dstIP, uint16(len(u)))
// Calculate the UDP checksum and set it.
xsum = header.Checksum(payload, xsum)
u.SetChecksum(^u.CalculateChecksum(xsum))
return []byte(buf)
}
func run(prog string, args ...string) error {
cmd := exec.Command(prog, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running %v: %v", cmd, err)
}
return nil
}
func (t *Wrapper) destMAC() [6]byte {
mac, _ := t.destMACAtomic.Load().([6]byte)
return mac
}
func (t *Wrapper) tapWrite(buf []byte, offset int) (int, error) {
if offset < ethernetFrameSize {
return 0, fmt.Errorf("[unexpected] weird offset %d for TAP write", offset)
}
eth := buf[offset-ethernetFrameSize:]
dst := t.destMAC()
copy(eth[:6], dst[:])
copy(eth[6:12], ourMAC[:])
et := etherTypeIPv4
if buf[offset]>>4 == 6 {
et = etherTypeIPv6
}
eth[12], eth[13] = et[0], et[1]
if tapDebug {
t.logf("tap: tapWrite off=%v % x", offset, buf)
}
return t.tdev.Write(buf, offset-ethernetFrameSize)
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !linux
// +build !linux
package tstun
func (*Wrapper) handleTAPFrame([]byte) bool { panic("unreachable") }
func (*Wrapper) tapWrite([]byte, int) (int, error) { panic("unreachable") }

View File

@@ -7,16 +7,15 @@
package tstun
import (
"bytes"
"errors"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
"golang.zx2c4.com/wireguard/tun"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
)
// tunMTU is the MTU we set on tailscale's TUN interface. wireguard-go
@@ -35,10 +34,32 @@ func init() {
}
}
// createTAP is non-nil on Linux.
var createTAP func(tapName, bridgeName string) (tun.Device, error)
// New returns a tun.Device for the requested device name, along with
// the OS-dependent name that was allocated to the device.
func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
dev, err := tun.CreateTUN(tunName, tunMTU)
var dev tun.Device
var err error
if strings.HasPrefix(tunName, "tap:") {
if runtime.GOOS != "linux" {
return nil, "", errors.New("tap only works on Linux")
}
f := strings.Split(tunName, ":")
var tapName, bridgeName string
switch len(f) {
case 2:
tapName = f[1]
case 3:
tapName, bridgeName = f[1], f[2]
default:
return nil, "", errors.New("bogus tap argument")
}
dev, err = createTAP(tapName, bridgeName)
} else {
dev, err = tun.CreateTUN(tunName, tunMTU)
}
if err != nil {
return nil, "", err
}
@@ -54,90 +75,18 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
return dev, name, nil
}
// tunDiagnoseFailure, if non-nil, does OS-specific diagnostics of why
// TUN failed to work.
var tunDiagnoseFailure func(tunName string, logf logger.Logf)
// Diagnose tries to explain a tuntap device creation failure.
// It pokes around the system and logs some diagnostic info that might
// help debug why tun creation failed. Because device creation has
// already failed and the program's about to end, log a lot.
func Diagnose(logf logger.Logf, tunName string) {
switch runtime.GOOS {
case "linux":
diagnoseLinuxTUNFailure(tunName, logf)
case "darwin":
diagnoseDarwinTUNFailure(tunName, logf)
default:
if tunDiagnoseFailure != nil {
tunDiagnoseFailure(tunName, logf)
} else {
logf("no TUN failure diagnostics for OS %q", runtime.GOOS)
}
}
func diagnoseDarwinTUNFailure(tunName string, logf logger.Logf) {
if os.Getuid() != 0 {
logf("failed to create TUN device as non-root user; use 'sudo tailscaled', or run under launchd with 'sudo tailscaled install-system-daemon'")
}
if tunName != "utun" {
logf("failed to create TUN device %q; try using tun device \"utun\" instead for automatic selection", tunName)
}
}
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf) {
kernel, err := exec.Command("uname", "-r").Output()
kernel = bytes.TrimSpace(kernel)
if err != nil {
logf("no TUN, and failed to look up kernel version: %v", err)
return
}
logf("Linux kernel version: %s", kernel)
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
if err == nil {
logf("'modprobe tun' successful")
// Either tun is currently loaded, or it's statically
// compiled into the kernel (which modprobe checks
// with /lib/modules/$(uname -r)/modules.builtin)
//
// So if there's a problem at this point, it's
// probably because /dev/net/tun doesn't exist.
const dev = "/dev/net/tun"
if fi, err := os.Stat(dev); err != nil {
logf("tun module loaded in kernel, but %s does not exist", dev)
} else {
logf("%s: %v", dev, fi.Mode())
}
// We failed to find why it failed. Just let our
// caller report the error it got from wireguard-go.
return
}
logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut)
switch distro.Get() {
case distro.Debian:
dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput()
if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil {
logf("tun module not loaded nor found on disk")
return
}
if !bytes.Contains(dpkgOut, kernel) {
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut)
}
case distro.Arch:
findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput()
if len(bytes.TrimSpace(findOut)) == 0 || err != nil {
logf("tun module not loaded nor found on disk")
return
}
if !bytes.Contains(findOut, kernel) {
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
}
case distro.OpenWrt:
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
if err != nil {
logf("error querying OpenWrt installed packages: %s", out)
return
}
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
if !bytes.Contains(out, []byte(pkg+" - ")) {
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
}
}
}
}

96
net/tstun/tun_linux.go Normal file
View File

@@ -0,0 +1,96 @@
// 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 tstun
import (
"bytes"
"os"
"os/exec"
"strings"
"syscall"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
)
func init() {
tunDiagnoseFailure = diagnoseLinuxTUNFailure
}
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf) {
var un syscall.Utsname
err := syscall.Uname(&un)
if err != nil {
logf("no TUN, and failed to look up kernel version: %v", err)
return
}
kernel := utsReleaseField(&un)
logf("Linux kernel version: %s", kernel)
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
if err == nil {
logf("'modprobe tun' successful")
// Either tun is currently loaded, or it's statically
// compiled into the kernel (which modprobe checks
// with /lib/modules/$(uname -r)/modules.builtin)
//
// So if there's a problem at this point, it's
// probably because /dev/net/tun doesn't exist.
const dev = "/dev/net/tun"
if fi, err := os.Stat(dev); err != nil {
logf("tun module loaded in kernel, but %s does not exist", dev)
} else {
logf("%s: %v", dev, fi.Mode())
}
// We failed to find why it failed. Just let our
// caller report the error it got from wireguard-go.
return
}
logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut)
switch distro.Get() {
case distro.Debian:
dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput()
if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil {
logf("tun module not loaded nor found on disk")
return
}
if !bytes.Contains(dpkgOut, []byte(kernel)) {
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut)
}
case distro.Arch:
findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput()
if len(bytes.TrimSpace(findOut)) == 0 || err != nil {
logf("tun module not loaded nor found on disk")
return
}
if !bytes.Contains(findOut, []byte(kernel)) {
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
}
case distro.OpenWrt:
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
if err != nil {
logf("error querying OpenWrt installed packages: %s", out)
return
}
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
if !bytes.Contains(out, []byte(pkg+" - ")) {
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
}
}
}
}
func utsReleaseField(u *syscall.Utsname) string {
var sb strings.Builder
for _, v := range u.Release {
if v == 0 {
break
}
sb.WriteByte(byte(v))
}
return strings.TrimSpace(sb.String())
}

27
net/tstun/tun_macos.go Normal file
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.
//go:build darwin && !ios
// +build darwin,!ios
package tstun
import (
"os"
"tailscale.com/types/logger"
)
func init() {
tunDiagnoseFailure = diagnoseDarwinTUNFailure
}
func diagnoseDarwinTUNFailure(tunName string, logf logger.Logf) {
if os.Getuid() != 0 {
logf("failed to create TUN device as non-root user; use 'sudo tailscaled', or run under launchd with 'sudo tailscaled install-system-daemon'")
}
if tunName != "utun" {
logf("failed to create TUN device %q; try using tun device \"utun\" instead for automatic selection", tunName)
}
}

View File

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

View File

@@ -8,8 +8,10 @@ package tstun
import (
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"sync/atomic"
"time"
@@ -21,6 +23,7 @@ import (
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/types/pad32"
"tailscale.com/wgengine/filter"
)
@@ -35,6 +38,8 @@ const PacketStartOffset = device.MessageTransportHeaderSize
// of a packet that can be injected into a tstun.Wrapper.
const MaxPacketSize = device.MaxContentSize
const tapDebug = false // for super verbose TAP debugging
var (
// ErrClosed is returned when attempting an operation on a closed Wrapper.
ErrClosed = errors.New("device closed")
@@ -61,13 +66,16 @@ type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
type Wrapper struct {
logf logger.Logf
// tdev is the underlying Wrapper device.
tdev tun.Device
tdev tun.Device
isTAP bool // whether tdev is a TAP device
closeOnce sync.Once
_ pad32.Four
lastActivityAtomic mono.Time // time of last send or receive
destIPActivity atomic.Value // of map[netaddr.IP]func()
destMACAtomic atomic.Value // of [6]byte
// buffer stores the oldest unconsumed packet from tdev.
// It is made a static buffer in order to avoid allocations.
@@ -146,10 +154,19 @@ type tunReadResult struct {
err error
}
func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper {
return wrap(logf, tdev, true)
}
func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
return wrap(logf, tdev, false)
}
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
tun := &Wrapper{
logf: logger.WithPrefix(logf, "tstun: "),
tdev: tdev,
logf: logger.WithPrefix(logf, "tstun: "),
isTAP: isTAP,
tdev: tdev,
// bufferConsumed is conceptually a condition variable:
// a goroutine should not block when setting it, even with no listeners.
bufferConsumed: make(chan struct{}, 1),
@@ -284,11 +301,14 @@ func allowSendOnClosedChannel() {
panic(r)
}
const ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype
// poll polls t.tdev.Read, placing the oldest unconsumed packet into t.buffer.
// This is needed because t.tdev.Read in general may block (it does on Windows),
// so packets may be stuck in t.outbound if t.Read called t.tdev.Read directly.
func (t *Wrapper) poll() {
for range t.bufferConsumed {
DoRead:
var n int
var err error
// Read may use memory in t.buffer before PacketStartOffset for mandatory headers.
@@ -303,7 +323,33 @@ func (t *Wrapper) poll() {
if t.isClosed() {
return
}
n, err = t.tdev.Read(t.buffer[:], PacketStartOffset)
if t.isTAP {
n, err = t.tdev.Read(t.buffer[:], PacketStartOffset-ethernetFrameSize)
if tapDebug {
s := fmt.Sprintf("% x", t.buffer[:])
for strings.HasSuffix(s, " 00") {
s = strings.TrimSuffix(s, " 00")
}
t.logf("TAP read %v, %v: %s", n, err, s)
}
} else {
n, err = t.tdev.Read(t.buffer[:], PacketStartOffset)
}
}
if t.isTAP {
if err == nil {
ethernetFrame := t.buffer[PacketStartOffset-ethernetFrameSize:][:n]
if t.handleTAPFrame(ethernetFrame) {
goto DoRead
}
}
// Fall through. We got an IP packet.
if n >= ethernetFrameSize {
n -= ethernetFrameSize
}
if tapDebug {
t.logf("tap regular frame: %x", t.buffer[PacketStartOffset:PacketStartOffset+n])
}
}
t.sendOutbound(tunReadResult{data: t.buffer[PacketStartOffset : PacketStartOffset+n], err: err})
}
@@ -521,6 +567,13 @@ func (t *Wrapper) Write(buf []byte, offset int) (int, error) {
}
t.noteActivity()
return t.tdevWrite(buf, offset)
}
func (t *Wrapper) tdevWrite(buf []byte, offset int) (int, error) {
if t.isTAP {
return t.tapWrite(buf, offset)
}
return t.tdev.Write(buf, offset)
}
@@ -553,7 +606,7 @@ func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error {
}
// Write to the underlying device to skip filters.
_, err := t.tdev.Write(buf, offset)
_, err := t.tdevWrite(buf, offset)
return err
}

View File

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

View File

@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64
//go:build !ios
// +build !ios
package portlist

View File

@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows freebsd openbsd darwin,go1.16 darwin,!go1.16,!arm64
//go:build (windows || freebsd || openbsd || darwin) && !ios
// +build windows freebsd openbsd darwin
// +build !ios
package portlist

View File

@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.16,ios !go1.16,darwin,!amd64
//go:build ios
// +build ios
package portlist

View File

@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,amd64,!go1.16 darwin,go1.16
// +build !ios
//go:build darwin && !ios
// +build darwin,!ios
package portlist

View File

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

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package safesocket
@@ -112,7 +113,7 @@ func socketPermissionsForOS() os.FileMode {
// 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
// nework extension is:
// network extension is:
//
// * the sandboxed IPNExtension picks a random localhost:0 TCP port
// to listen on

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