Compare commits

...

222 Commits

Author SHA1 Message Date
Aaron Klotz
96188ffd2f ipn/ipnserver, log/filelogger, logpolicy: when tailscaled is running as a windows service, ensure the service's log messages are written to file
This patch moves the Windows-only initialization of the filelogger into logpolicy.
Previously we only did it when babysitting the tailscaled subprocess, but this meant
that log messages from the service itself never made it to disk. This meant that
if logtail could not dial out, its log messages would be lost.

I modified filelogger.New to work a bit differently and added a `maybeWrapForPlatform`
to logpolicy to ensure that the filelogger is plugged in front of logtail ASAP.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2021-12-15 13:05:14 -07:00
Brad Fitzpatrick
486059589b all: gofmt -w -s (simplify) tests
And it updates the build tag style on a couple files.

Change-Id: I84478d822c8de3f84b56fa1176c99d2ea5083237
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-15 08:43:41 -08:00
Brad Fitzpatrick
59f4f33f60 cmd/tailscaled: fix windows logtail integration
I broke it in 1.17.x sometime while rewiring some logs stuff,
mostly in 0653efb092 (but with a handful
of logs-related changes around that time)

Fixes tailscale/corp#3265

Change-Id: Icb5c07412dc6d55f1d9244c5d0b51dceca6a7e34
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-14 15:03:37 -08:00
Maya Kaczorowski
ac8e69b713 Merge pull request #3330 from tailscale/mayakacz-patch-2
README.md: update platforms
2021-12-13 17:25:02 -08:00
Maya Kaczorowski
0f3b55c299 README.md: update platforms
Update platform support. This matches content in https://tailscale.com/kb/1062/reviewer-guide/#which-platforms-does-it-run-on

Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2021-12-13 17:11:07 -08:00
Josh Bleecher Snyder
4691e012a9 tstest/integration: build binaries only once
The existing code relied on the Go build cache to avoid
needless work when obtaining the tailscale binaries.

For non-obvious reasons, the binaries were getting re-linked
every time, which added 600ms or so on my machine to every test.

Instead, build the binaries exactly once, on demand.
This reduces the time to run 'go test -count=5' from 34s to 10s
on my machine.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-13 14:38:08 -08:00
Denton Gentry
e133bb570b install.sh: add linuxmint, kali, several more.
After apt install, Kali Linux had not enabled nor started
the tailscaled systemd service. Add a quirks mode to enable
and start it after apt install for debian platforms.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-12-13 11:38:29 -08:00
Brad Fitzpatrick
adc97e9c4d cmd/tailscale: make --accept-routes default true on Windows, macOS GUI
One of the most annoying parts of using the Tailscale CLI on Windows
and the macOS GUI is that Tailscale's GUIs default to running with
"Route All" (accept all non-exitnode subnet routes) but the CLI--being
originally for Linux--uses the Linux default, which is to not accept
subnets.

Which means if a Windows user does, e.g.:

    tailscale up --advertise-exit-node
Or:
    tailscale up --shields-up

... then it'd warn about reverting the --accept-routes option, which the user
never explicitly used.

Instead, make the CLI's default match the platform/GUI's default.

Change-Id: I15c804b3d9b0266e9ca8651e0c09da0f96c9ef8d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-13 10:33:20 -08:00
Maisem Ali
d24a8f7b5a wgengine/router{windows}: return the output from the firewallTweaker
on error.

While debugging a customer issue where the firewallTweaker was failing
the only message we have is `router: firewall: error adding
Tailscale-Process rule: exit status 1` which is not really helpful.
This will help diagnose firewall tweaking failures.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-12-13 10:07:32 -08:00
David Crawshaw
8dbda1a722 scripts/installer.sh: press Y on RHEL
For #3540

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-12-13 09:48:41 -08:00
Brad Fitzpatrick
cced414c7d net/dns/resolver: add Windows ExitDNS service support, using net package
Updates #1713
Updates #835

Change-Id: Ia71e96d0632c2d617b401695ad68301b07c1c2ec
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-10 20:47:17 -08:00
Brad Fitzpatrick
cab5c46481 net/dns: bound how long we block looking for, restarting systemd-resolved
Fixes #3537

Change-Id: Iba6a3cde75983490d4072b5341f48dbfa2f997c0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-10 09:58:14 -08:00
Josh Bleecher Snyder
63cd581c3f safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.

The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.

If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.

Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-09 15:46:38 -08:00
Josh Bleecher Snyder
a5235e165c tstest/integration: fix running with -verbose-tailscale
Without this fix, any run with -verbose-tailscale fails.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-09 15:46:38 -08:00
Josh Bleecher Snyder
c8829b742b all: minor code cleanup
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-09 15:46:38 -08:00
Brad Fitzpatrick
39ffa16853 net/dnscache, net/tsdial: add DNS caching to tsdial UserDial
This is enough to handle the DNS queries as generated by Go's
net package (which our HTTP/SOCKS client uses), and the responses
generated by the ExitDNS DoH server.

This isn't yet suitable for putting on 100.100.100.100 where a number
of different DNS clients would hit it, as this doesn't yet do
EDNS0. It might work, but it's untested and likely incomplete.

Likewise, this doesn't handle anything about truncation, as the
exchanges are entirely in memory between Go or DoH. That would also
need to be handled later, if/when it's hooked up to 100.100.100.100.

Updates #3507

Change-Id: I1736b0ad31eea85ea853b310c52c5e6bf65c6e2a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-09 11:34:21 -08:00
Brad Fitzpatrick
b59e7669c1 wgengine/netstack: in netstack/hybrid mode, fake ICMP using ping command
Change-Id: I42cb4b9b326337f4090d9cea532230e36944b6cb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-09 09:30:10 -08:00
Brad Fitzpatrick
21741e111b net/packet: add ICMP6Header, like ICMP4Header
So we can generate IPv6 ping replies.

Change-Id: I79a9a38d8aa242e5dfca4cd15dfaffaea6cb1aee
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-09 09:30:10 -08:00
Brad Fitzpatrick
7b9c7bc42b ipn/ipnstate: remove old deprecated TailAddr IPv4-only field
It's been a bunch of releases now since the TailscaleIPs slice
replacement was added.

Change-Id: I3bd80e1466b3d9e4a4ac5bedba8b4d3d3e430a03
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-09 09:28:23 -08:00
Brad Fitzpatrick
affc4530a2 net/packet: don't make IP6Header.marshalPseudo assume UDP
It will be used for ICMPv6 next, so pass in the proto.

Also, use the ipproto constants rather than hardcoding the mysterious
number.

Change-Id: I57b68bdd2d39fff75f82affe955aff9245de246b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-08 21:15:46 -08:00
Brad Fitzpatrick
485bcdc951 net/packet: fix doc copy/paste-o
Change-Id: I0aca490b3ccb0c124192afb362a10b19a15a3e2b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-08 21:12:43 -08:00
Denton Gentry
878a20df29 net/dns: add GetBaseConfig to CallbackRouter.
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates https://github.com/tailscale/tailscale/issues/2116
Updates https://github.com/tailscale/tailscale/issues/988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-12-08 16:49:11 -08:00
Brad Fitzpatrick
a28d280b95 cmd/tailscaled: move start-up failure logging to one place
The caller of func run said:

    // No need to log; the func already did

But that wasn't true. Some return paths didn't log.

So instead, return rich errors and have func main do the logging,
so we can't miss anything in the future.

Prior to this, safesocket.Listen for instance was causing tailscaled
to os.Exit(1) on failure without any clue as to why.

Change-Id: I9d71cc4d73d0fed4aa1b1902cae199f584f25793
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-08 15:13:39 -08:00
David Anderson
9f867ad2c5 .github/dependabot.yml: disable eager updates for Go.
Given our development cycle, we'll instead do big-bang updates
after every release, to give time for all the updates to soak in
unstable.

This does _not_ disable dependabot security-critical PRs.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-12-08 10:37:03 -08:00
Brad Fitzpatrick
c0701b130d ipn/ipnstate, cmd/tailscale: add Online bool to tailscale status & --json
Fixes #3533

Change-Id: I2f6f0d712cf3f987fba1c15be74cdb5c8d565f04
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-08 09:34:15 -08:00
Arnaud Dezandee
656809e4ee cmd/derper: allow http port configuration
Signed-off-by: Arnaud Dezandee <dezandee.arnaud@gmail.com>
2021-12-08 08:58:30 -08:00
root
e34ba3223c version/distro: report TrueNAS Scale as "truenas"
TrueNAS Scale is based on Debian Linux

Signed-off-by: Todd Neal <todd@tneal.org>
2021-12-07 21:04:58 -08:00
Todd Neal
c18dc57861 ipn/{ipnserver,ipnlocal}: support incoming Taildrop on TrueNAS
Signed-off-by: Todd Neal <todd@tneal.org>
2021-12-07 21:04:58 -08:00
Denton Gentry
ffb16cdffb cmd/tailscale: add --json for up
Print JSON to stdout containing everything needed for
authentication.

{
  "AuthURL": "https://login.tailscale.com/a/0123456789",
  "QR": "data:image/png;base64,iV...QmCC",
  "BackendState": "NeedsLogin"
}
{
  "BackendState": "Running"
}

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-12-06 21:17:15 -08:00
Brad Fitzpatrick
d3d503d997 ipn/ipnlocal: add HTTP/2 h2c server support to peerapi on non-mobile platforms
To make ExitDNS cheaper.

Might not finish client-side support in December before 1.20, but at
least server support can start rolling out ahead of clients being
ready for it.

Tested with curl against peerapi.

Updates #1713

Change-Id: I676fed5fb1aef67e78c542a3bc93bddd04dd11fe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-06 16:39:14 -08:00
Brad Fitzpatrick
abc00e9c8d ipn/{ipnserver,ipnlocal}: support incoming Taildrop on Synology
If the user has a "Taildrop" shared folder on startup and
the "tailscale" system user has read/write access to it,
then the user can "tailscale file cp" to their NAS.

Updates #2179 (would be fixes, but not super ideal/easy yet)

Change-Id: I68e59a99064b302abeb6d8cc84f7d2a09f764990
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-06 16:36:24 -08:00
David Anderson
190b7a4cca go.mod: mass update with go get -u.
Gets ahead of dependabot slightly, but the updates are minor.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-12-06 13:00:37 -08:00
dependabot[bot]
0d8ef1ff35 go.mod: bump github.com/aws/aws-sdk-go-v2/service/ssm
Bumps [github.com/aws/aws-sdk-go-v2/service/ssm](https://github.com/aws/aws-sdk-go-v2) from 1.17.0 to 1.17.1.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.17.0...service/ssm/v1.17.1)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/ssm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 11:27:21 -08:00
dependabot[bot]
329751c48e go.mod: bump golang.org/x/tools from 0.1.7 to 0.1.8
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.7 to 0.1.8.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.7...v0.1.8)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 11:20:43 -08:00
dependabot[bot]
9ddef8cdbf go.mod: bump github.com/mdlayher/netlink from 1.4.1 to 1.4.2
Bumps [github.com/mdlayher/netlink](https://github.com/mdlayher/netlink) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/mdlayher/netlink/releases)
- [Changelog](https://github.com/mdlayher/netlink/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mdlayher/netlink/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: github.com/mdlayher/netlink
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 11:05:15 -08:00
dependabot[bot]
9140f193bc go.mod: bump github.com/aws/aws-sdk-go-v2/feature/s3/manager
Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.7.3 to 1.7.4.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/fsx/v1.7.3...feature/s3/manager/v1.7.4)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 11:02:21 -08:00
dependabot[bot]
05c1be3e47 .github: Bump actions/upload-artifact from 1 to 2.2.4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 2.2.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v2.2.4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 11:01:40 -08:00
David Anderson
e6e63c2305 .github/dependabot.yml: make dependabot send all the updates right now.
So we can mass-process updates once now, then turn it off.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-12-06 10:57:33 -08:00
Brad Fitzpatrick
c0984f88dc Makefile: add spk and pushspk targets for iterative Synology development
Change-Id: I97319d14917aa2b00ff72a7b73b3db79ea8392b7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-06 09:52:06 -08:00
Todd Neal
eeccbccd08 support running in a FreeBSD jail
Since devd apparently can't be made to work in a FreeBSD jail
fall back to polling.

Fixes tailscale#2858

Signed-off-by: Todd Neal <todd@tneal.org>
2021-12-05 21:42:52 -08:00
Brad Fitzpatrick
69de3bf7bf wgengine/filter: let unknown IPProto match if IP okay & match allows all ports
RELNOTE=yes

Change-Id: I96eaf3cf550cee7bb6cdb4ad81fc761e280a1b2a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-05 10:44:18 -08:00
Josh Bleecher Snyder
1813c2a162 .: add .gitattributes entry to use Go hunk-header driver
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-03 17:56:02 -08:00
Maisem Ali
0a9932f3b2 build_docker.sh: prefix version strings with v
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-12-03 13:55:09 -08:00
Brad Fitzpatrick
9c5c9d0a50 ipn/ipnlocal, net/tsdial: make SOCKS/HTTP dials use ExitDNS
And simplify, unexport some tsdial/netstack stuff in the the process.

Fixes #3475

Change-Id: I186a5a5cbd8958e25c075b4676f7f6e70f3ff76e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-03 13:39:37 -08:00
Brad Fitzpatrick
9f6249b26d ipn/policy: treat DNS service as interesting so it's not filtered out
The control plane is currently still eating it.

Updates #1713

Change-Id: I66a0698599d6794ab1302f9585bf29e38553c884
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-03 12:55:54 -08:00
Josh Bleecher Snyder
de635ac0a8 cmd/tailscale: print more detail in connection failure error message
Before:

failed to connect to local tailscaled (which appears to be running). Got error: Get "http://local-tailscaled.sock/localapi/v0/status": EOF

After:

failed to connect to local tailscaled (which appears to be running as IPNExtension, pid 2118). Got error: Get "http://local-tailscaled.sock/localapi/v0/status": EOF

This was useful just now, as it made it clear that tailscaled I thought
I was connecting to might not in fact be running; there was
a second tailscaled running that made the error message slightly misleading.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-03 12:46:50 -08:00
Brad Fitzpatrick
003089820d cmd/tailscale: fix setting revert checker's finding an exit node
It was using the wrong prefs (intended vs current) to map the current
exit node ID to an IP.

Fixes #3480

Change-Id: I9f117d99a84edddb4cd1cb0df44a2f486abde6c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-02 21:19:48 -08:00
Brad Fitzpatrick
03a323de4e cmd/tailscale: don't allow setting exit node to known invalid value
Fixes #3081

Change-Id: I34c378dfd51ee013c21962dbe79c49b1ba06c0c5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-02 14:34:33 -08:00
Brad Fitzpatrick
a8f60cf6e8 cmd/tailscale: clarify which prefless flags don't need revert protection
Fixes #3482

Change-Id: Icb51476b0d78d3758cb04df3b565e372b8289a46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-02 14:07:00 -08:00
Brad Fitzpatrick
f91481075d cmd/tailscale: let --exit-node= take a machine name in addition to IP
If you're online, let tailscale up --exit-node=NAME map NAME to its IP.

We don't store the exit node name server-side in prefs, avoiding
the concern raised earlier.

Fixes #3062

Change-Id: Ieea5ceec1a30befc67e9d6b8a530b3cb047b6b40
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-02 14:02:55 -08:00
Brad Fitzpatrick
adc5997592 net/tsdial: give netstack a Dialer, start refactoring name resolution
This starts to refactor tsdial.Dialer's name resolution to have
different stages: in-memory MagicDNS vs system resolution. A future
change will plug in ExitDNS resolution.

This also plumbs a Dialer into netstack and unexports the dnsMap
internals.

And it removes some of the async AddNetworkMapCallback usage and
replaces it with synchronous updates of the Dialer's netmap
from LocalBackend, since the LocalBackend has the Dialer too.

Updates #3475

Change-Id: Idcb7b1169878c74f0522f5151031ccbc49fe4cb4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-02 11:33:13 -08:00
Josh Bleecher Snyder
768baafcb5 tstest/integration: use t.Cleanup
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-02 11:11:11 -08:00
Brad Fitzpatrick
43983a4a3b ipn/ipnlocal: run peerapi even if Taildrop storage not configured
Change-Id: I77f9ecbe4617d01d13aa1127fa59c83f2aa3e1b8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 16:45:09 -08:00
David Anderson
44d0c1ab06 ipn/ipnlocal: resolve exit node IP to ID at EditPrefs time.
Without this, enabling an exit node immediately blackholes all traffic,
but doesn't correctly let it flow to the exit node until the next netmap
update.

Fixes #3447

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-12-01 15:50:32 -08:00
Brad Fitzpatrick
8775c646be net/tsdial: make dialing to peerapi work in netstack mode
With this, I'm able to send a Taildrop file (using "tailscale file cp")
from a Linux machine running --tun=userspace-networking.

Updates #2179

Change-Id: I4e7a4fb0fbda393e4fb483adb06b74054a02cfd0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 14:16:34 -08:00
Brad Fitzpatrick
ad3d6e31f0 net/tsdial: move macOS/iOS peerapi sockopt logic from LocalBackend
Change-Id: I812cae027c40c70cdc701427b1a1850cd9bcd60c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 12:55:31 -08:00
Josh Bleecher Snyder
25eab78573 control/noise: clean up resources in TestNoReuse
Close the server and client.
Without this, we leak system threads.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-01 12:50:21 -08:00
Brad Fitzpatrick
c7fb26acdb net/tsdial: also plumb TUN name and monitor into tsdial.Dialer
In prep for moving stuff out of LocalBackend.

Change-Id: I9725aa9c3ebc7275f8c40e040b326483c0340127
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 10:36:55 -08:00
Brad Fitzpatrick
c37af58ea4 net/tsdial: move more weirdo dialing into new tsdial package, plumb
Not done yet, but this move more of the outbound dial special casing
from random packages into tsdial, which aspires to be the one unified
place for all outbound dialing shenanigans.

Then this plumbs it all around, so everybody is ultimately
holding on to the same dialer.

As of this commit, macOS/iOS using an exit node should be able to
reach to the exit node's DoH DNS proxy over peerapi, doing the sockopt
to stay within the Network Extension.

A number of steps remain, including but limited to:

* move a bunch more random dialing stuff

* make netstack-mode tailscaled be able to use exit node's DNS proxy,
  teaching tsdial's resolver to use it when an exit node is in use.

Updates #1713

Change-Id: I1e8ee378f125421c2b816f47bc2c6d913ddcd2f5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 10:36:55 -08:00
Brad Fitzpatrick
bf1d69f25b wgengine/monitor: fix docs on Mon.InterfaceState
The behavior was changed in March (in 7f174e84e6)
but that change forgot to update these docs.

Change-Id: I79c0301692c1d13a4a26641cc5144baf48ec1360
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-01 10:36:06 -08:00
Josh Bleecher Snyder
2075c39fd7 net/portmapper: deflake TestPCPIntegration
Logging in goroutines after the test completed
caused data races and panics. Prevent that.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-01 10:13:27 -08:00
Artyom Pervukhin
49a9e62d58 Replace AWS SDK v1 dependency with v2
This change drops AWS SDKv1 dependency, leaving only SDK v2 in use.

Closes #3461

Signed-off-by: Artyom Pervukhin <github@artyom.dev>
2021-12-01 07:51:22 -08:00
Danny Hermes
56c72d9cde godoc fix: StatusWithPeers -> StatusWithoutPeers
Signed-off-by: Danny Hermes <daniel.j.hermes@gmail.com>
2021-11-30 23:17:06 -08:00
Brad Fitzpatrick
d5405c66b7 net/tsdial: start of new package to unify all outbound dialing complexity
For now this just deletes the net/socks5/tssocks implementation (and
the DNSMap stuff from wgengine/netstack) and moves it into net/tsdial.

Then initialize a Dialer early in tailscaled, currently only use for the
outbound and SOCKS5 proxies. It will be plumbed more later. Notably, it
needs to get down into the DNS forwarder for exit node DNS forwading
in netstack mode. But it will also absorb all the peerapi setsockopt
and netns Dial and tlsdial complexity too.

Updates #1713

Change-Id: Ibc6d56ae21a22655b2fa1002d8fc3f2b2ae8b6df
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 17:21:49 -08:00
Brad Fitzpatrick
3ae6f898cf ipn/ipnlocal, net/dns/resolver: use exit node's DoH proxy when available
Updates #1713

Change-Id: I3695a40ec12d2b4e6dac41cf4559daca6dddd68e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 17:01:09 -08:00
Brad Fitzpatrick
16abd7e07c ipn/ipnlocal: fix Content-Length in DoH DNS proxy response
Updates #1713

Change-Id: I912d90383b751ad97b32bcec55e8fedbcf4d3db8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 17:01:09 -08:00
Brad Fitzpatrick
2a95ee4680 cmd/tailscale, ipn/ipnstate: note which nodes are exit nodes in status
Fixes #3446

Change-Id: Ib41d588e7fa434c02d134fa449f85b0e15083683
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 16:59:23 -08:00
Josh Bleecher Snyder
deb2f5e793 types/logger: add Context and related helpers
We often need both a log function and a context.
We can do this by adding the log function as a context value.
This commit adds helper glue to make that easy.
It is designed to allow incremental adoption.

Updates tailscale/corp#3138

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 15:18:21 -08:00
Aaron Klotz
f93cf6fa03 net/dns: fix checking for wrapped error when attempting to read wsl.conf for Windows WSL2
Fixes https://github.com/tailscale/tailscale/issues/3437

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2021-11-30 15:36:39 -07:00
Josh Bleecher Snyder
b800663779 tstest/integration: stop leaking zstd.Decoders
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 14:08:05 -08:00
David Anderson
124363e0ca net/dns: detect and decode UTF-16 from wsl.exe earlier.
Fixes #3170

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-30 13:10:18 -08:00
Josh Bleecher Snyder
e16cb523aa net/nettest: deflake TestPipeTimeout
The block-write and block-read tests are both flaky,
because each assumes it can get a normal read/write
completed within 10ms. This isn’t always true.

We can’t increase the timeouts, because that slows down the test.
However, we don’t need to issue a regular read/write for this test.
The immediately preceding tests already test this code,
using a far more generous timeout.

Remove the extraneous read/write.

This drops the failure rate from 1 per 20,000 to undetectable
on my machine.

While we’re here, fix a typo in a debug print statement.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:21:59 -08:00
Josh Bleecher Snyder
a8cc519c70 net/portmapper: improve handling of UPnP parse errors
Without the continue, we might overwrite our current meta
with a zero meta.

Log the error, so that we can check for anything unexpected.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Josh Bleecher Snyder
fddf43f3d1 net/portmapper: fill out PCP/PMP client metrics
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Josh Bleecher Snyder
9787ec6f4a net/portmapper: add UPnP client metrics
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Josh Bleecher Snyder
40f11c50a1 net/portmapper: make PCP/PMP result codes stringers
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Josh Bleecher Snyder
38d90fa330 net/portmapper: add clientmetrics for PCP/PMP responses
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Josh Bleecher Snyder
999814e9e1 net/portmapper: handle pcp ADDRESS_MISMATCH response
These show up a fair amount in our logs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-30 12:13:15 -08:00
Brad Fitzpatrick
bb91cfeae7 net/socks5/tssocks, wgengine: permit SOCKS through subnet routers/exit nodes
Fixes #1970

Change-Id: Ibef45e8796e1d9625716d72539c96d1dbf7b1f76
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 11:54:14 -08:00
Brad Fitzpatrick
3181bbb8e4 cmd/tailscale: make file cp send files via tailscaled localapi
So Taildrop sends work even if the local tailscaled is running in
netstack mode, as it often is on Synology, etc.

Updates #2179 (which is primarily about receiving, but both important)

Change-Id: I9bd1afdc8d25717e0ab6802c7cf2f5e0bd89a3b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-30 11:47:27 -08:00
David Crawshaw
46a9782322 .github/dependabot.yml: slow down the stream of unusable PRs
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-11-30 09:27:14 -08:00
dependabot[bot]
d89c61b812 go.mod: bump github.com/aws/aws-sdk-go-v2/service/ssm
Bumps [github.com/aws/aws-sdk-go-v2/service/ssm](https://github.com/aws/aws-sdk-go-v2) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.16.0...service/s3/v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/ssm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-30 08:01:44 -08:00
dependabot[bot]
341e1af873 go.mod: bump github.com/aws/aws-sdk-go-v2/config from 1.10.2 to 1.10.3
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.10.2...config/v1.10.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-30 05:48:56 -08:00
Brad Fitzpatrick
b811a316bc tailcfg, ipn/ipnlocal: advertise a Service when exit node DNS proxy available
Updates #1713

Change-Id: I20c8e2ad1062d82ef17363414e372133f4c7181e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 21:57:06 -08:00
David Anderson
6e584ffa33 cmd/tailscaled: allow running the SOCKS5 and HTTP proxies on the same port.
Fixes #3248

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-29 16:49:48 -08:00
David Anderson
a54d13294f net/proxymux: add a listener mux that can run SOCKS and HTTP on a single socket.
Updates #3248

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-29 16:49:48 -08:00
Brad Fitzpatrick
135580a5a8 tailcfg, ipn/ipnlocal, net/dns: forward exit node DNS on Unix to system DNS
Updates #1713

Change-Id: I4c073fec0992d9e01a9a4ce97087d5af0efdc68d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 15:25:41 -08:00
Josh Bleecher Snyder
d9c21936c3 control/controlclient: stop logging about goal.url invariant
This isn't the ideal solution, but it's good enough for now.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-29 14:00:53 -08:00
David Crawshaw
1e8b4e770a update github.com/aws/aws-sdk-go-v2
Replaces #3464, #3365, #3366 with a PR that includes the depaware fix.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-11-29 12:10:28 -08:00
Brad Fitzpatrick
105c545366 cmd/tailscale/cli: don't complain about --accept-routes true->false on Synology
Fixes #3176

Change-Id: I844883e741dccfa5e7771c853180e9f65fb7f7a4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 11:21:38 -08:00
Brad Fitzpatrick
c2efe46f72 ipn/ipnlocal: restrict exit node DoH server based on ACL'ed packet filter
Don't be a DoH DNS server to peers unless the Tailnet admin has permitted
that peer autogroup:internet access.

Updates #1713

Change-Id: Iec69360d8e4d24d5187c26904b6a75c1dabc8979
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 09:56:59 -08:00
Brad Fitzpatrick
ff9727c9ff wgengine/filter: fix, test NewAllowAllForTest
I probably broke it when SCTP support was added but nothing apparently
ever used NewAllowAllForTest so it wasn't noticed when it broke.

Change-Id: Ib5a405be233d53cb7fcc61d493ae7aa2d1d590a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 09:56:59 -08:00
Thomas Weiß
f8cef1ba08 ipn/store/aws: support using sub-paths in parameters
Fixes #3431

Signed-off-by: Thomas Weiß <panos@unbunt.org>
2021-11-29 07:48:01 -08:00
Thomas Weiß
6dc6ea9b37 cmd/tailscaled: log error on state store init failure
Signed-off-by: Thomas Weiß <panos@unbunt.org>
2021-11-29 07:48:01 -08:00
Brad Fitzpatrick
78b0bd2957 net/dns/resolver: add clientmetrics for DNS
Fixes tailscale/corp#1811

Change-Id: I864d11e0332a177e8c5ff403591bff6fec548f5a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-26 17:57:48 -08:00
David Anderson
097602b3ca ipn/ipnlocal: warn more precisely about IP forwarding issues on linux.
If IP forwarding is disabled globally, but enabled per-interface on all interfaces,
don't complain. If only some interfaces have forwarding enabled, warn that some
subnet routing/exit node traffic may not work.

Fixes #1586

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 11:03:10 -08:00
David Anderson
db800ddeac cmd/derper: set Content-Security-Policy on DERPs.
It's a basic "deny everything" policy, since DERP's HTTP
server is very uninteresting from a browser POV. But it
stops every security scanner under the sun from reporting
"dangerously configured" HTTP servers.

Updates tailscale/corp#3119

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 11:00:44 -08:00
David Anderson
33c541ae30 ipn/ipnlocal: populate self status from netmap in ipnlocal, not magicsock.
Fixes #1933

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 10:56:42 -08:00
Denton Gentry
e121c2f724 logpolicy: export NewLogtailTransport for Android
Android doesn't use logpolicy and currently has enough
unique stuff about its logging that makes it difficult to
do so. For example, its logsDir comes from Gio.

Export NewLogtailTransport to let Android use it.

Updates https://github.com/tailscale/tailscale/issues/3046

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-26 07:45:13 -08:00
Brad Fitzpatrick
25525b7754 net/dns/resolver, ipn/ipnlocal: wire up peerapi DoH server to DNS forwarder
Updates #1713

Change-Id: Ia4ed9d8c9cef0e70aa6d30f2852eaab80f5f695a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-23 18:59:36 -08:00
Maya Kaczorowski
9bb91cb977 Merge pull request #3214 from tailscale/mayakacz-patch-1
.github: feature request template change
2021-11-23 19:09:51 -05:00
Maya Kaczorowski
259163dfe1 Update feature_request.yml
Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2021-11-23 18:52:52 -05:00
Denton Gentry
f56a7559ce scripts/installer.sh: add more Linux variants.
Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-23 15:12:29 -08:00
Josh Bleecher Snyder
d10cefdb9b net/dns: require space after nameserver/search parsing resolv.conf
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 15:11:46 -08:00
Josh Bleecher Snyder
9f00510833 net/dns: handle comments in resolv.conf
Currently, comments in resolv.conf cause our parser to fail,
with error messages like:

ParseIP("192.168.0.100 # comment"): unexpected character (at " # comment")

Fix that.

Noticed while looking through logs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 15:11:46 -08:00
Josh Bleecher Snyder
955aa188b3 ipn/ipnlocal: fix logging
We were missing an argument here.
Also, switch to %q, in case anything weird
is happening with these strings.

Updates tailscale/corp#461

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 13:36:59 -08:00
Josh Bleecher Snyder
73beaaf360 net/tstun: rate limit "self disco out packet" logging
When this happens, it is incredibly noisy in the logs.
It accounts for about a third of all remaining
"unexpected" log lines from a recent investigation.

It's not clear that we know how to fix this,
we have a functioning workaround,
and we now have a (cheap and efficient) metric for this
that we can use for measurements.

So reduce the logging to approximately once per minute.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:52:52 -08:00
Josh Bleecher Snyder
b0d543f7a1 cmd/tailscale: add ip -1 flag
This limits the output to a single IP address.

RELNOTE=tailscale ip now has a -1 flag (TODO: update docs to use it)

Fixes #1921

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:05:32 -08:00
Josh Bleecher Snyder
73beaf59fb cmd/tailscale: improve ip subcommand docs
Streamline the prose.
Clarify what peer may be.
Improve an error message.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:05:32 -08:00
Joonas Loppi
a3b709f0c4 tsshd: fix double exit with different exit codes
Signed-off-by: Joonas Loppi <joonas@joonas.fi>
2021-11-23 09:19:59 -08:00
Brad Fitzpatrick
283ae702c1 ipn/ipnlocal: start adding DoH DNS server to peerapi when exit node
Updates #1713

Change-Id: I8d9c488f779e7acc811a9bc18166a2726198a429
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-23 08:21:41 -08:00
dependabot[bot]
6fd6fe11f2 go.mod: bump honnef.co/go/tools from 0.2.1 to 0.2.2
Bumps [honnef.co/go/tools](https://github.com/dominikh/go-tools) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/dominikh/go-tools/releases)
- [Commits](https://github.com/dominikh/go-tools/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: honnef.co/go/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 22:01:32 -08:00
Josh Bleecher Snyder
027b46d0c1 ipn/ipnstate: clarify PeerStatusLite.LastHandshake
And document the other fields, as long as we're here.

Updates #1182

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 22:01:20 -08:00
Brad Fitzpatrick
0de1b74fbb util/clientmetric: add tests omitted from earlier commit
These were supposed to be part of
3b541c833e but I guess I forgot to "git
add" them. Whoops.

Updates #3307

Change-Id: I8c768a61ec7102a01799e81dc502a22399b9e9f0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-22 21:49:28 -08:00
Josh Bleecher Snyder
ad5e04249b wgengine/monitor: ignore adding/removing uninteresting IPs
One of the most common "unexpected" log lines is:

"network state changed, but stringification didn't"

One way that this can occur is if an interesting interface
(non-Tailscale, has interesting IP address)
gains or loses an uninteresting IP address (link local or loopback).

The fact that the interface is interesting is enough for EqualFiltered
to inspect it. The fact that an IP address changed is enough for
EqualFiltered to declare that the interfaces are not equal.

But the State.String method reasonably declines to print any
uninteresting IP addresses. As a result, the network state appears
to have changed, but the stringification did not.

The String method is correct; nothing interesting happened.

This change fixes this by adding an IP address filter to EqualFiltered
in addition to the interface filter. This lets the network monitor
ignore the addition/removal of uninteresting IP addresses.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 16:33:15 -08:00
Josh Bleecher Snyder
60510a6ae7 .github/workflows: check that repo is clean after build and test
Linux-only for now, to avoid having to figure out why
powershell doesn't like my shell scripting. (Not that I blame it.)
That'll be enough to catch most regressions.

Fixes #1083

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 15:28:59 -08:00
Denton Gentry
1ea270375a hostinfo: report when running in Docker Desktop.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-22 13:45:54 -08:00
Josh Bleecher Snyder
ca1b3fe235 net/tshttpproxy: use correct size for Windows BOOL argument
The Windows BOOL type is an int32. We were using a bool,
which is a one byte wide. This could be responsible for the
ERROR_INVALID_PARAMETER errors we were seeing for calls to
WinHttpGetProxyForUrl.

We manually checked all other existing Windows syscalls
for similar mistakes and did not find any.

Updates #879

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 12:24:24 -08:00
David Anderson
9a217ec841 cmd/derper: increase HSTS cache lifetime to 2 years.
Fixes #3373.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-22 11:59:01 -08:00
Maisem Ali
9feb483ad3 build_docker.sh: use github.com/tailscale/mkctr instead of docker
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-11-22 11:39:30 -08:00
Aaron Klotz
7d8feb2784 hostinfo: change Windows implementation to directly query version information using API and registry
We replace the cmd.exe invocation with RtlGetNtVersionNumbers for the first
three fields. On Windows 10+, we query for the fourth field which is available
via the registry.

The fourth field is not really documented anywhere; Firefox has been querying
it successfully since Windows 10 was released, so we can be pretty confident in
its longevity at this point.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2021-11-22 12:26:42 -07:00
Josh Bleecher Snyder
1a629a4715 net/portmapper: mark fewer PMP probe failures as unexpected
There are lots of lines in the logs of the form:

portmapper: unexpected PMP probe response: {OpCode:128 ResultCode:3
SecondsSinceEpoch:NNN MappingValidSeconds:0 InternalPort:0
ExternalPort:0 PublicAddr:0.0.0.0}

ResultCode 3 here means a network failure, e.g. the NAT box itself has
not obtained a DHCP lease. This is not an indication that something
is wrong in the Tailscale client, so use different wording here
to reflect that. Keep logging, so that we can analyze and debug
the reasons that PMP probes fail.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 11:13:15 -08:00
Brad Fitzpatrick
e8db43e8fa wgengine/router: demote TestDebugListRules fail to skip
Updates #3360

Change-Id: Ic5c98ea03f3171c13ab9293a0ae74d17fd04d149
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-22 11:04:45 -08:00
David Anderson
937e96f43d cmd/derper: enable HSTS when serving over HTTPS.
Starting with a short lifetime, to verify nothing breaks.

Updates #3373

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-22 09:57:34 -08:00
dependabot[bot]
f76a8d93da go.mod: bump github.com/godbus/dbus/v5 from 5.0.5 to 5.0.6
Bumps [github.com/godbus/dbus/v5](https://github.com/godbus/dbus) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/godbus/dbus/releases)
- [Commits](https://github.com/godbus/dbus/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: github.com/godbus/dbus/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 08:40:09 -08:00
Brad Fitzpatrick
2ea765e5d8 go.mod: bump inet.af/netstack
Updates #2642 (I'd hoped, but doesn't seem to fix it)

Change-Id: Id54af7c90a1206bc7018215957e20e954782b911
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-21 09:18:31 -08:00
AdamKorcz
def659d1ec Fuzzing: Add CIFuzz
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-11-19 13:06:20 -08:00
Brad Fitzpatrick
946dfec98a wgengine/router: fix checkIPRuleSupportsV6 to actually use IPv6
Updates #3358 (should fix it)
Updates #391

Change-Id: Ia62437dfa81247b0b5994d554cf279c3d540e4e7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 11:37:05 -08:00
Brad Fitzpatrick
9259377a7f wgengine/router: don't assume Linux was built with IP_MULTIPLE_TABLES
Updates #3351
Updates #391

Change-Id: I7e66b686e05f3c970846513679cc62556ebe322a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 11:19:03 -08:00
David Anderson
88b8a09d37 net/dns: make constants for the various DBus strings.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
6c82cebe57 health: add a health state for net/dns.OSConfigurator.
Lets the systemd-resolved OSConfigurator report health changes
for out of band config resyncs.

Updates #3327

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
4ef3fed100 net/dns: resync config to systemd-resolved when it restarts.
Fixes #3327

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
cf9169e4be net/dns: remove unused Config struct element.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
Brad Fitzpatrick
0350cf0438 wgengine{,/router}: annotate some more errors
Updates #3351

Change-Id: I8b4f957d2051b3e29401bb449dbadbdada3a7c46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 10:46:01 -08:00
Brad Fitzpatrick
5294125e7a cmd/tailscaled: disambiguate some startup failure error messages
Updates #3351

Change-Id: I0afead4a084623567f56b19187574fa97b295b2a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 08:58:29 -08:00
Josh Bleecher Snyder
758c37b83d net/netns: thread logf into control functions
So that darwin can log there without panicking during tests.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-18 15:09:51 -08:00
Josh Bleecher Snyder
85184a58ed wgengine/wgcfg: recover from mismatched PublicKey/Endpoints
In rare circumstances (tailscale/corp#3016), the PublicKey
and Endpoints can diverge.

This by itself doesn't cause any harm, but our early exit
in response did, because it prevented us from recovering from it.

Remove the early exit.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-18 14:28:41 -08:00
Denton Gentry
9fc4e876e3 VERSION.txt: this is v1.19.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-18 12:12:48 -08:00
Brad Fitzpatrick
8ec44d0d5f wgengine/magicsock: remove some log spam
Fixes tailscale/corp#3070

Change-Id: Ie50031800ec8669e0596ad6d59d1e329a5c88516
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 11:01:51 -08:00
Brad Fitzpatrick
61d0435ed9 wgengine/monitor: reduce Windows log spam
Fixes #3345

Change-Id: Icde9c92f88f98bb3b030d39b0424a7d389bceb88
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 10:57:27 -08:00
Brad Fitzpatrick
0653efb092 cmd/tailscaled: remove a redundant date prefix on Windows logs
Change-Id: I28e122d4384697f51a748d67829409276c00b11e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 10:23:41 -08:00
Brad Fitzpatrick
49a3fcae78 log/filelogger: make filelogger remove redundant date before adding a date
At some point since filelogger was added on Windows, the log hierarchy
above it changed such that a log.Printf writes to filelogger and includes
the log package's own date. But then filelogger adds another.

Rather than debug everything above and risk removing the prefix when
run by tailscaled, instead just remove the log package's prefix
very late right before we go to add the filelogger's own.

Change-Id: I9db518f42c603ef83017f74827270f124fdf5c14
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 10:23:41 -08:00
Brad Fitzpatrick
4a59a2781a ipn/ipnlocal: export client metrics over peerapi to owner
Updates #3307

Change-Id: I41b1f3c16af5f385575e8d6cea70ae8386504dd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 08:04:00 -08:00
Brad Fitzpatrick
d24ed3f68e wgengine/router: add debug knob to resort to Linux "ip" command usage
Tailscale 1.18 uses netlink instead of the "ip" command to program the
Linux kernel.

The old way was kept primarily for tests, but this also adds a
TS_DEBUG_USE_IP_COMMAND environment knob to force the old way
temporarily for debugging anybody who might have problems with the
new way in 1.18.

Updates #391

Change-Id: I0236fbfda6c9c05dcb3554fcc27ec0c86456efd9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-18 08:01:22 -08:00
Josh Bleecher Snyder
b3d6704aa3 wgengine/magicsock: fix data race on endpoint.discoKey
endpoint.discoKey is protected by endpoint.mu.
endpoint.sendDiscoMessage was reading it without holding the lock.
This showed up in a CI failure and is readily reproducible locally.

The fix is in two parts.

First, for Conn.enqueueCallMeMaybe, eliminate the one-line helper method endpoint.sendDiscoMessage; call Conn.sendDiscoMessage directly.
This makes it more natural to read endpoint.discoKey in a context
in which endpoint.mu is already held.

Second, for endpoint.sendDiscoPing, explicitly pass the disco key
as an argument. Again, this makes it easier to read endpoint.discoKey
in a context in which endpoint.mu is already held.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-17 17:49:33 -08:00
Brad Fitzpatrick
cf06f9df37 net/tstun, wgengine: add packet-level and drop metrics
Primarily tstun work, but some MagicDNS stuff spread into wgengine.

No wireguard reconfig metrics (yet).

Updates #3307

Change-Id: Ide768848d7b7d0591e558f118b553013d1ec94ad
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-17 16:18:52 -08:00
Brad Fitzpatrick
ec036b3561 logpolicy: use bootstrap DNS for logtail dialer
Fixes #3332

Change-Id: Ie45efb448e5508c3ece48dd1d8d7e9a39e2e9dc1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-17 14:37:43 -08:00
Brad Fitzpatrick
7901289578 wgengine/magicsock: add a stress test
And add a peerMap validate method that checks its internal invariants.

Updates tailscale/corp#3016

Change-Id: I23708e68ed44d81986d9e2be82029d4555547592
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-17 14:37:28 -08:00
Josh Bleecher Snyder
5a60781919 wgengine/magicsock: increase TestDiscokeyChange connection timeout
I believe that this should eliminate the flakiness.
If GitHub CI manages to be even slower that can be believed
(and I can believe a lot at this point),
then we should roll this back and make some more invasive changes.

Updates #654
Fixes #3247 (I hope)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-17 14:13:58 -08:00
Brad Fitzpatrick
5b5f032c9a util/clientmetric: optimize memory layout for finding updates
Updates #3307

Change-Id: I2840b190583467cc3f00688b96ce3d170df46a46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-17 12:30:49 -08:00
Josh Bleecher Snyder
773af7292b wgengine/magicsock: simplify peerMap.upsertEndpoint
We can do the "maybe delete" check unilaterally:
In the case of an insert, both oldDiscoKey
and ep.discoKey will be the zero value.

And since we don't use pi again, we can skip
giving it a name, which makes scoping clearer.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-16 15:15:49 -08:00
Josh Bleecher Snyder
9da22dac3d wgengine/magicsock: fix bug in peerMap.upsertEndpoint
Found by inspection by David Crawshaw while
investigating tailscale/corp#3016.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-16 15:15:49 -08:00
Josh Bleecher Snyder
16870cb754 wgengine/magicsock: fix typo in comment
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-16 15:15:49 -08:00
Brad Fitzpatrick
36b1df1241 cmd/tailscale/cli: add --watch flag to "debug metrics" subcommand
This adds a new --watch flag that prints out a block of metric changes
every second, if anything changed.

Example output:

magicsock_disco_recv_ping    +1 => 254
magicsock_disco_recv_pong    +1 => 218
magicsock_disco_recv_udp     +2 => 472
magicsock_disco_send_udp     +2 => 536
magicsock_disco_sent_udp     +2 => 536
magicsock_recv_data_ipv6     +1 => 82
magicsock_send_data          +1 => 86
magicsock_send_udp           +3 => 620

magicsock_recv_data_ipv6    +1 => 83
magicsock_send_data         +1 => 87
magicsock_send_udp          +1 => 621

magicsock_disco_recv_ping    +1 => 255
magicsock_disco_recv_pong    +1 => 219
magicsock_disco_recv_udp     +2 => 474
magicsock_disco_send_udp     +2 => 538
magicsock_disco_sent_udp     +2 => 538
magicsock_recv_data_ipv6     +1 => 84
magicsock_send_data          +1 => 88
magicsock_send_udp           +3 => 624

magicsock_recv_data_ipv6    +1 => 85
magicsock_send_data         +1 => 89
magicsock_send_udp          +1 => 625

controlclient_map_response_map          +1 => 207
controlclient_map_response_map_delta    +1 => 204
controlclient_map_response_message      +1 => 275
magicsock_disco_recv_ping               +3 => 258
magicsock_disco_recv_pong               +2 => 221
magicsock_disco_recv_udp                +5 => 479
magicsock_disco_send_derp               +1 => 6
magicsock_disco_send_udp                +7 => 545
magicsock_disco_sent_derp               +1 => 6
magicsock_disco_sent_udp                +7 => 545
magicsock_recv_data_ipv6                +1 => 86
magicsock_send_data                     +1 => 90
magicsock_send_derp                     +1 => 12
magicsock_send_derp_queued              +1 => 12
magicsock_send_udp                      +8 => 633

Updates #3307

Change-Id: I5ac2511e3ad24fa1e6ea958c3946fecebe4f79a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-16 13:48:21 -08:00
David Anderson
41da7620af go.mod: update wireguard-go to pick up roaming toggle
wgengine/wgcfg: introduce wgcfg.NewDevice helper to disable roaming
at all call sites (one real plus several tests).

Fixes tailscale/corp#3016.

Signed-off-by: David Anderson <danderson@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-16 13:15:04 -08:00
Brad Fitzpatrick
400ed799e6 net/dns: work around old systemd-resolved setLinkDomain length limit
Don't set all the *.arpa. reverse DNS lookup domains if systemd-resolved
is old and can't handle them.

Fixes #3188

Change-Id: I283f8ce174daa8f0a972ac7bfafb6ff393dde41d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-16 12:54:21 -08:00
Brian Fallik
9fa6cdf7bf fix minor typo
Signed-off-by: Brian Fallik <bfallik@gmail.com>
2021-11-16 11:03:43 -08:00
Brad Fitzpatrick
24ea365d48 netcheck, controlclient, magicsock: add more metrics
Updates #3307

Change-Id: Ibb33425764a75bde49230632f1b472f923551126
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-16 10:48:19 -08:00
Brad Fitzpatrick
3b541c833e util/clientmetric, logtail: log metric changes
Updates #3307

Change-Id: I1399ebd786f6ff7defe6e11c0eb651144c071574
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-16 08:06:31 -08:00
Brad Fitzpatrick
68917fdb5d cmd/tailscale/cli: add "debug metrics" subcommand
To let users inspect the tailscaled metrics easily.

Updates #3307

Change-Id: I922126ca0626659948c57de74c6ef62f40ef5f5f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-15 15:13:25 -08:00
Brad Fitzpatrick
945290cc3f cmd/tailscale/cli: migrate hidden debug subcommand to use subcomands
It was a mess of flags. Use subcommands under "debug" instead.

And document loudly that it's not a stable interface.

Change-Id: Idcc58f6a6cff51f72cb5565aa977ac0cc30c3a03
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-15 15:03:58 -08:00
Brad Fitzpatrick
57b039c51d util/clientmetrics: add new package to add metrics to the client
And annotate magicsock as a start.

And add localapi and debug handlers with the Prometheus-format
exporter.

Updates #3307

Change-Id: I47c5d535fe54424741df143d052760387248f8d3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-15 13:46:05 -08:00
David Anderson
c5d572f371 net/dns: correctly handle NetworkManager-managed DNS that points to resolved.
Fixes #3304

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-15 12:21:25 -08:00
Brad Fitzpatrick
f7da8c77bd tstest/integration/testcontrol: fix data race
Fix race from 1ec99e99f4

Fixes #3289

Change-Id: I58158d3f82339ac171fb14827c5f158d602327f4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-11 08:25:16 -08:00
David Anderson
5b94f67956 control/noise: make Conn.readNLocked less surprising.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
a34350ffda control/noise: factor out nonce checking and incrementing into a type.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
d3acd35a90 control/noise: make message headers match the specification.
Only the initiation message should carry a protocol version, all
others are just type+len.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
a63c4ab378 control/noise: don't panic when handling ciphertext.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
4004b22fe5 control/noise: stop using poly1305 package constants.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
293431aaea control/noise: use key.Machine{Public,Private} as appropriate.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
edb33d65c3 control/noise: don't cache mixer, just rebuild a BLAKE2s each time.
This should optimize out fine, and readability is preferable to performance
here.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
7e9e72887c control/noise: add singleUseCHP, use it to simplify nonce/key tracking in handshake.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
cf90392174 control/noise: review fixups
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
0b392dbaf7 control/noise: adjust implementation to match revised spec.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
89a68a4c22 control/noise: include the protocol version in the Noise prologue.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
5e005a658f control/noise: fix typo in docstring.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
eabca699ec control/noise: remove allocations in the encrypt and decrypt paths.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
David Anderson
da7544bcc5 control/noise: implement the base transport for the 2021 control protocol.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-10 12:13:54 -08:00
Brad Fitzpatrick
3e1daab704 hostinfo, control/controlclient: tell control when Ubuntu has disabled Tailscale's sources
Fixes #3177
Updates #2500

Change-Id: Iff2a8e27ec7d36a1c210263d6218f20ebed37924
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-10 09:56:58 -08:00
Brad Fitzpatrick
d2ef73ed82 control/controlclient: rename a variable to not shadow a package name
Change-Id: I1bcb577cb2c47e936d545ad57f308e57399de323
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-10 08:11:29 -08:00
Maisem Ali
d6dde5a1ac ipn/ipnlocal: handle key extensions after key has already expired
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-11-08 18:15:09 -08:00
Maisem Ali
eccc2ac6ee net/interfaces/windows: update Tailscale interface detection logic to
account for new wintun naming.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-11-08 07:44:33 -08:00
Brad Fitzpatrick
ad63fc0510 control/controlclient: make js/wasm work with Go 1.18+
Updates #3157

Change-Id: I2d67e582842ab3638d720bb5db4701b878ad4473
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-07 13:49:55 -08:00
Brad Fitzpatrick
87137405e5 ipn/ipnserver: grant js/wasm all localapi permissions
Updates #3157

Change-Id: I3b63762583a4d655eac33ce3dfda37a1f5135a57
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-07 12:13:15 -08:00
Brad Fitzpatrick
40e13c316c paths: add missing js/wasm stub
Change-Id: Iae4838f5fa1dc0cd491d5a3ac906fd3cdacb173c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-07 12:13:15 -08:00
Brad Fitzpatrick
0edd2d1cd5 safesocket: add js/wasm implementation with in-memory net.Conn
Updates #3157

Change-Id: Ia35b1e259011fb86f8c4e01f62146f9fd4c9b7c6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-07 12:13:14 -08:00
Brad Fitzpatrick
01bd789c26 ipn/ipnserver: add Server.LocalBackend accessor
Was done as part of e6fbc0cd54 for ssh
work, but wasn't committed yet. Including it here both to minimize the
ssh diff size, and because I need it for a separate change.

Change-Id: If6eb54a2ca7150ace96488ed14582c2c05ca3422
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-07 11:31:52 -08:00
Michael Stapelberg
b3abdc381d tsnet: set varRoot state directory field
This makes tsnet work on https://gokrazy.org! 🎉

Signed-off-by: Michael Stapelberg <michael@stapelberg.de>
2021-11-07 10:56:23 -08:00
Brad Fitzpatrick
e6fbc0cd54 cmd/tailscaled, ipn/ipnserver: refactor ipnserver
More work towards removing the massive ipnserver.Run and ipnserver.Options
and making composable pieces.

Work remains. (The getEngine retry loop on Windows complicates things.)
For now some duplicate code exists. Once the Windows side is fixed
to either not need the retry loop or to move the retry loop into a
custom wgengine.Engine wrapper, then we can unify tailscaled_windows.go
too.

Change-Id: If84d16e3cd15b54ead3c3bb301f27ae78d055f80
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-05 15:00:02 -07:00
Brad Fitzpatrick
5f36ab8a90 tstest/integration: go generate
Change-Id: I49d19007a16261e447240e149deac24c15c93fce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-05 14:43:51 -07:00
Brad Fitzpatrick
2b082959db safesocket: add WindowsLocalPort const
Remove all the 41112 references.

Change-Id: I2d7ed330d457e3bb91b7e6416cfb2667611e50c4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-05 14:05:13 -07:00
Denton Gentry
1ec99e99f4 tstest: extend node key expiration integration test.
Can produce the problem in #2515, preparing to test a fix.
Marked as t.Skip() until we have a fix.

Updates https://github.com/tailscale/tailscale/issues/2515

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-04 11:46:42 -07:00
dependabot[bot]
12148dcf48 go.mod: bump github.com/frankban/quicktest from 1.13.1 to 1.14.0
Bumps [github.com/frankban/quicktest](https://github.com/frankban/quicktest) from 1.13.1 to 1.14.0.
- [Release notes](https://github.com/frankban/quicktest/releases)
- [Commits](https://github.com/frankban/quicktest/compare/v1.13.1...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/frankban/quicktest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-04 09:39:31 -07:00
Brad Fitzpatrick
337757a819 ipn/ipnlocal, control/controlclient: don't propagate all map errors to UI
Fixes regression from 81cabf48ec which made
all map errors be sent to the frontend UI.

Fixes #3230

Change-Id: I7f142c801c7d15e268a24ddf901c3e6348b6729c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-03 17:56:54 -07:00
David Anderson
0532eb30db all: replace tailcfg.DiscoKey with key.DiscoPublic.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-03 14:00:16 -07:00
Mangirdas
f771327f0c Add multiarch image make target
Updates #3112

Signed-off-by: Mangirdas <mangirdas@judeikis.lt>
2021-11-03 13:13:20 -07:00
Brad Fitzpatrick
649f7556e8 cmd/tailscaled, ipn: add tailscaled --statedir flag for var directory
Fixes #2932

Change-Id: I1aa2b323ad542386d140f8336bcc4dcbb8310bd0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-03 13:12:38 -07:00
Brad Fitzpatrick
c7bff35fee ipn/ipnlocal: add owner-only debug handler to get process env
For debugging Synology. Like the existing goroutines handler, in that
it's owner-only.

Change-Id: I852f0626be8e1c0b6794c1e062111d14adc3e6ac
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-03 13:12:32 -07:00
Brad Fitzpatrick
6d82a18916 tstest/integration: don't include stdlib deps in go generate output
Causes too much churn for zero benefit.

Change-Id: I838f8cdb5723f122f11dd4bbce5e9c07755c3cd9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-03 11:59:59 -07:00
Josh Bleecher Snyder
c467ed0b62 wgengine/wgcfg: always close io.Pipe
In DeviceConfig, we did not close r after calling FromUAPI.
If FromUAPI returned early due to an error, then it might
not have read all the data that IpcGetOperation wanted to write.
As a result, IpcGetOperation could hang, as in #3220.

We were also closing the wrong end of the pipe after IpcSetOperation
in ReconfigDevice.

To ensure that we get all available information to diagnose
such a situation, include all errors anytime something goes wrong.

This should fix the immediate crashing problem in #3220.
We'll then need to figure out why IpcGetOperation was failing.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-02 17:50:15 -07:00
Josh Bleecher Snyder
3fd5f4380f util/multierr: new package
github.com/go-multierror/multierror served us well.
But we need a few feature from it (implement Is),
and it's not worth maintaining a fork of such a small module.

Instead, I did a clean room implementation inspired by its API.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-02 17:50:15 -07:00
David Anderson
17b5782b3a types/key: delete legacy NodeKey type.
Fixes #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-02 14:14:32 -07:00
David Anderson
7e6a1ef4f1 tailcfg: use key.NodePublic in wire protocol types.
Updates #3206.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-02 09:11:43 -07:00
David Anderson
7e8d5ed6f3 ipn: use key.NodePublic instead of tailcfg.NodeKey
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-01 20:32:10 -07:00
David Anderson
c17250cee2 ipn/ipnstate: use key.NodePublic instead of tailcfg.NodeKey.
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-01 20:32:10 -07:00
David Anderson
c3d7115e63 wgengine: use key.NodePublic instead of tailcfg.NodeKey.
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-01 18:28:45 -07:00
David Anderson
72ace0acba wgengine/magicsock: use key.NodePublic instead of tailcfg.NodeKey.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-01 18:03:48 -07:00
David Anderson
d6e7cec6a7 types/netmap: use key.NodePublic instead of tailcfg.NodeKey.
Update #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-01 17:07:40 -07:00
Brad Fitzpatrick
408b0923a6 wgengine/router: remove last non-test "ip" command usage on Linux
Updates #391

Change-Id: Ic2c3f8460b1e4b8d34b936a1725705fcc1effbae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 15:52:24 -07:00
Brad Fitzpatrick
ff1954cfd9 wgengine/router: use netlink for ip rules on Linux
Using temporary netlink fork in github.com/tailscale/netlink until we
get the necessary changes upstream in either vishvananda/netlink
or jsimonetti/rtnetlink.

Updates #391

Change-Id: I6e1de96cf0750ccba53dabff670aca0c56dffb7c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 15:40:36 -07:00
Brad Fitzpatrick
5dc5bd8d20 cmd/tailscaled, wgengine/netstack: always wire up netstack
Even if not in use. We plan to use it for more stuff later.

(not for iOS or macOS-GUIs yet; only tailscaled)

Change-Id: Idaef719d2a009be6a39f158fd8f57f8cca68e0ee
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 14:11:30 -07:00
Brad Fitzpatrick
ff597e773e tailcfg, control/controlclient: add method to exit client from control plane
Change-Id: Ic28ef283ba63396b68fab86bfb0a8ee8f432474c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 11:59:04 -07:00
Brad Fitzpatrick
0303ec44c3 go.mod: bump netstack for mipsle fix
Fixes #3233

Change-Id: I18d1af886402774ce0ecc77dae3bc71eb8ba5c9d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 11:23:05 -07:00
Brad Fitzpatrick
c18b9d58aa tstest/archtest: add GOARCH-specific tests, run via qemu-user
Updates #3233

Change-Id: Ia224c90490d41e50a1d547eeea709b0d9171c1f9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-01 11:17:43 -07:00
Xe
b02eb1d5c5 scripts/installer: handle fedora (#3235)
We missed a switch case.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-11-01 12:29:48 -04:00
oocococo
3a2b0fc36c cmd/derper: support custom TLS port when in manual mode (#3231)
Fixes #3232

Change-Id: I8dae5c01f9dfdfd6d45e34e4ca3534b642ae5c8e
Signed-off-by: oocococo <mercurial.lx@gmail.com>
2021-10-31 18:31:49 -07:00
David Anderson
8d14bc32d1 tstest/integration: use key.NodePublic instead of tailcfg.NodeKey.
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 17:49:16 -07:00
David Anderson
84c3a09a8d types/key: export constants for key size, not a method.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 17:39:04 -07:00
David Anderson
6422789ea0 disco: use key.NodePublic instead of tailcfg.NodeKey.
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 17:39:04 -07:00
David Anderson
0fcc88873b tailcfg: remove NodeKeyFromNodePublic.
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 16:35:32 -07:00
David Anderson
c0ae1d2563 tailcfg: update go generate, which apparently normalizes type aliases.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 16:24:38 -07:00
David Anderson
418adae379 various: use NodePublic.AsNodeKey() instead of tailcfg.NodeKeyFromNodePublic()
Updates #3206

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-10-29 16:19:27 -07:00
209 changed files with 12049 additions and 2386 deletions

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
go.mod filter=go-mod
*.go diff=golang

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Support requests and Troubleshooting
- name: Support
url: https://tailscale.com/contact/support/
about: Contact us for support
- name: Troubleshooting
url: https://tailscale.com/kb/1023/troubleshooting
about: Troubleshoot common issues. Contact us by email at support@tailscale.com.
about: Troubleshoot common issues

View File

@@ -7,14 +7,7 @@ body:
attributes:
value: |
Please check if your feature request is [already filed](https://github.com/tailscale/tailscale/issues).
- type: input
id: request
attributes:
label: Tell us about your idea!
description: What is your feature request?
placeholder: e.g., A pet pangolin
validations:
required: true
Tell us about your idea!
- type: textarea
id: problem
attributes:

View File

@@ -2,15 +2,20 @@
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "go.mod:"
## Disabled between releases. We reenable it briefly after every
## stable release, pull in all changes, and close it again so that
## the tree remains more stable during development and the upstream
## changes have time to soak before the next release.
# - package-ecosystem: "gomod"
# directory: "/"
# schedule:
# interval: "daily"
# commit-message:
# prefix: "go.mod:"
# open-pull-requests-limit: 100
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
commit-message:
prefix: ".github:"

26
.github/workflows/cifuzz.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: CIFuzz
on: [pull_request]
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
dry-run: false
language: go
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 300
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v2.2.4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts

View File

@@ -31,6 +31,21 @@ jobs:
- name: Run tests and benchmarks with -race flag on linux
run: go test -race -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -28,9 +28,33 @@ jobs:
- name: Basic build
run: go build ./cmd/...
- name: Get QEMU
run: |
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
# use this PPA which brings in a modern qemu.
sudo add-apt-repository -y ppa:jacob/virtualisation
sudo apt-get -y update
sudo apt-get -y install qemu-user
- name: Run tests on linux
run: go test -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -31,6 +31,21 @@ jobs:
- name: Run tests on linux
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -48,13 +48,13 @@ ARG VERSION_SHORT=""
ENV VERSION_SHORT=$VERSION_SHORT
ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN go install -tags=xversion -ldflags="\
RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled
FROM alpine:3.14
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
FROM ghcr.io/tailscale/alpine-base:3.14
COPY --from=build-env /go/bin/* /usr/local/bin/

6
Dockerfile.base Normal file
View File

@@ -0,0 +1,6 @@
# 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.
FROM alpine:3.14
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -1,3 +1,5 @@
IMAGE_REPO ?= tailscale/tailscale
usage:
echo "See Makefile"
@@ -21,7 +23,18 @@ build386:
buildlinuxarm:
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
./build_docker.sh
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
spk:
go run github.com/tailscale/tailscale-synology@main --version=build -o tailscale.spk --source=.
pushspk: spk
echo "Pushing SPKG to root@${SYNOHOST} (env var SYNOHOST) ..."
scp tailscale.spk root@${SYNOHOST}:
ssh root@${SYNOHOST} /usr/syno/bin/synopkg install tailscale.spk

View File

@@ -8,11 +8,12 @@ Private WireGuard® networks made easy
This repository contains all the open source Tailscale client code and
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
daemon runs primarily on Linux; it also works to varying degrees on
FreeBSD, OpenBSD, Darwin, and Windows.
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
The Android app is at https://github.com/tailscale/tailscale-android
The Synology package is at https://github.com/tailscale/tailscale-synology
## Using
We serve packages for a variety of distros at

View File

@@ -1 +1 @@
1.17.0
1.19.0

View File

@@ -30,12 +30,14 @@ else
fi
long_suffix="$change_suffix-t$short_hash"
SHORT="$major.$minor.$patch"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"

View File

@@ -21,8 +21,15 @@ set -eu
eval $(./build_dist.sh shellvars)
docker build \
--build-arg VERSION_LONG=$VERSION_LONG \
--build-arg VERSION_SHORT=$VERSION_SHORT \
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
-t tailscale:$VERSION_SHORT -t tailscale:latest .
go run github.com/tailscale/mkctr@latest \
--base="ghcr.io/tailscale/alpine-base:3.14" \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--tags="v${VERSION_SHORT},v${VERSION_MINOR}" \
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
--push

View File

@@ -38,6 +38,9 @@ var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
TailscaledSocketSetExplicitly bool
// TailscaledDialer is the DialContext func that connects to the local machine's
// tailscaled or equivalent.
TailscaledDialer = defaultDialer
@@ -47,7 +50,8 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
if TailscaledSocket == paths.DefaultTailscaledSocket() {
// TODO: make this part of a safesocket.ConnectionStrategy
if !TailscaledSocketSetExplicitly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
@@ -56,7 +60,11 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
return safesocket.Connect(TailscaledSocket, 41112)
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
}
var (
@@ -90,6 +98,27 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
return tsClient.Do(req)
}
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
path := req.URL.Path
pathPrefix := path
if i := strings.Index(path, "?"); i != -1 {
pathPrefix = path[:i]
}
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
type errorJSON struct {
Error string
}
@@ -140,23 +169,11 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
res, err := doLocalRequestNiceError(req)
if err != nil {
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
pathPrefix := path
if i := strings.Index(path, "?"); i != -1 {
pathPrefix = path[:i]
}
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
defer res.Body.Close()
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
@@ -196,6 +213,12 @@ func Goroutines(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func DaemonMetrics(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/metrics")
}
// Profile returns a pprof profile of the Tailscale daemon.
func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
@@ -222,7 +245,7 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")
}
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "?peers=false")
}
@@ -289,6 +312,30 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
return fts, nil
}
// PushFile sends Taildrop file r to target.
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return fmt.Errorf("%s: %s", res.Status, all)
}
func CheckIPForwarding(ctx context.Context) error {
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {

View File

@@ -12,6 +12,7 @@ import (
"errors"
"expvar"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
@@ -36,6 +37,7 @@ import (
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server address")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
@@ -141,7 +143,7 @@ func main() {
cfg := loadConfig()
serveTLS := tsweb.IsProd443(*addr)
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
s := derp.NewServer(cfg.PrivateKey, log.Printf)
s.SetVerifyClient(*verifyClients)
@@ -235,24 +237,41 @@ func main() {
cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil
}
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, "80"),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
}
err := port80srv.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set HTTP headers to appease automated security scanners.
//
// Security automation gets cranky when HTTPS sites don't
// set HSTS, and when they don't specify a content
// security policy for XSS mitigation.
//
// DERP's HTTP interface is only ever used for debug
// access (for which trivial safe policies work just
// fine), and by DERP clients which don't obey any of
// these browser-centric headers anyway.
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
if *httpPort > -1 {
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
}
}
}()
err := port80srv.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
}
}()
}
err = httpsrv.ListenAndServeTLS("", "")
} else {
log.Printf("derper: serving on %s", *addr)

View File

@@ -164,6 +164,11 @@ change in the future.
}
tailscale.TailscaledSocket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
tailscale.TailscaledSocketSetExplicitly = true
}
})
err := rootCmd.Run(context.Background())
if errors.Is(err, flag.ErrHelp) {
@@ -191,7 +196,8 @@ var rootArgs struct {
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
c, err := safesocket.Connect(rootArgs.socket, 41112)
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
c, err := safesocket.Connect(s)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")

View File

@@ -18,8 +18,10 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/version/distro"
)
// geese is a collection of gooses. It need not be complete.
@@ -57,6 +59,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
curExitNodeIP netaddr.IP
curUser string // os.Getenv("USER") on the client side
goos string // empty means "linux"
distro distro.Distro
want string
}{
@@ -313,6 +316,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
RouteAll: true,
// And assume this no-op accidental pre-1.8 value:
NoSNAT: true,
@@ -329,7 +333,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
},
goos: "windows",
goos: "openbsd",
want: "", // not an error
},
{
@@ -405,6 +409,21 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeAllowLANAccess: true,
ExitNodeID: "some_stable_id",
},
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
},
{
name: "ignore_login_server_synonym",
flags: []string{"--login-server=https://controlplane.tailscale.com"},
@@ -427,6 +446,38 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: distro.Synology,
want: "",
},
{
// Same test case as "synology_permit_omit_accept_routes" above, but
// on non-Synology distro.
name: "not_synology_dont_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: "", // not Synology
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -447,6 +498,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
distro: tt.distro,
}); err != nil {
got = err.Error()
}
@@ -495,6 +547,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
WantRunning: true,
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
},
@@ -532,7 +585,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
args: upArgsT{
exitNodeIP: "foo",
},
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
},
{
name: "error_exit_node_allow_lan_without_exit_node",
@@ -806,3 +859,133 @@ func TestUpdatePrefs(t *testing.T) {
})
}
}
func TestExitNodeIPOfArg(t *testing.T) {
mustIP := netaddr.MustParseIP
tests := []struct {
name string
arg string
st *ipnstate.Status
want netaddr.IP
wantErr string
}{
{
name: "ip_while_stopped_okay",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Stopped",
},
want: mustIP("1.2.3.4"),
},
{
name: "ip_not_found",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
},
wantErr: `no node found in netmap with IP 1.2.3.4`,
},
{
name: "ip_not_exit",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
},
},
},
wantErr: `node 1.2.3.4 is not advertising an exit node`,
},
{
name: "ip",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
ExitNodeOption: true,
},
},
},
want: mustIP("1.2.3.4"),
},
{
name: "no_match",
arg: "unknown",
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`,
},
{
name: "name",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
},
},
want: mustIP("1.0.0.2"),
},
{
name: "name_not_exit",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
},
},
},
wantErr: `node "skippy" is not advertising an exit node`,
},
{
name: "ambiguous",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
key.NewNode().Public(): {
DNSName: "SKIPPY.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
},
},
wantErr: `ambiguous exit node name "skippy"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := exitNodeIPOfArg(tt.arg, tt.st)
if err != nil {
if err.Error() == tt.wantErr {
return
}
if tt.wantErr == "" {
t.Fatal(err)
}
t.Fatalf("error = %#q; want %#q", err, tt.wantErr)
}
if tt.wantErr != "" {
t.Fatalf("got %v; want error %#q", got, tt.wantErr)
}
if got != tt.want {
t.Fatalf("got %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -5,6 +5,8 @@
package cli
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
@@ -14,7 +16,9 @@ import (
"log"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
@@ -24,39 +28,76 @@ import (
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
Name: "debug",
Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
fs.BoolVar(&debugArgs.derpMap, "derp", false, "If true, dump DERP map")
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
fs.BoolVar(&debugArgs.env, "env", false, "dump environment")
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
return fs
})(),
Subcommands: []*ffcli.Command{
{
Name: "derp-map",
Exec: runDERPMap,
ShortHelp: "print DERP map",
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "print tailscaled's goroutines",
},
{
Name: "metrics",
Exec: runDaemonMetrics,
ShortHelp: "print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics")
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
return fs
})(),
},
{
Name: "env",
Exec: runEnv,
ShortHelp: "print cmd/tailscale environment",
},
{
Name: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale local API",
},
{
Name: "prefs",
Exec: runPrefs,
ShortHelp: "print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
return fs
})(),
},
{
Name: "watch-ipn",
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
return fs
})(),
},
},
}
var debugArgs struct {
env bool
localCreds bool
goroutines bool
ipn bool
netMap bool
derpMap bool
file string
prefs bool
pretty bool
cpuSec int
cpuFile string
memFile string
file string
cpuSec int
cpuFile string
memFile string
}
func writeProfile(dst string, v []byte) error {
@@ -81,26 +122,9 @@ func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if debugArgs.env {
for _, e := range os.Environ() {
outln(e)
}
return nil
}
if debugArgs.localCreds {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
printf("curl http://localhost:41112/localapi/v0/status\n")
return nil
}
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
var usedFlag bool
if out := debugArgs.cpuFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
return err
@@ -112,6 +136,7 @@ func runDebug(ctx context.Context, args []string) error {
}
}
if out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing memory profile ...")
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
return err
@@ -122,55 +147,8 @@ func runDebug(ctx context.Context, args []string) error {
log.Printf("Memory profile written to %s", outName(out))
}
}
if debugArgs.prefs {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if debugArgs.pretty {
outln(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
outln(string(j))
}
return nil
}
if debugArgs.goroutines {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {
return err
}
Stdout.Write(goroutines)
return nil
}
if debugArgs.derpMap {
dm, err := tailscale.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
)
}
enc := json.NewEncoder(Stdout)
enc.SetIndent("", "\t")
enc.Encode(dm)
return nil
}
if debugArgs.ipn {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.SetNotifyCallback(func(n ipn.Notify) {
if !debugArgs.netMap {
n.NetMap = nil
}
j, _ := json.MarshalIndent(n, "", "\t")
printf("%s\n", j)
})
bc.RequestEngineStatus()
pump(ctx, bc, c)
return errors.New("exit")
}
if debugArgs.file != "" {
usedFlag = true // TODO(bradfitz): add "file" subcommand
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
@@ -193,5 +171,148 @@ func runDebug(ctx context.Context, args []string) error {
io.Copy(Stdout, rc)
return nil
}
if usedFlag {
// TODO(bradfitz): delete this path when all debug flags are migrated
// to subcommands.
return nil
}
return errors.New("see 'tailscale debug --help")
}
func runLocalCreds(ctx context.Context, args []string) error {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
return nil
}
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
var prefsArgs struct {
pretty bool
}
func runPrefs(ctx context.Context, args []string) error {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if prefsArgs.pretty {
outln(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
outln(string(j))
}
return nil
}
var watchIPNArgs struct {
netmap bool
}
func runWatchIPN(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.SetNotifyCallback(func(n ipn.Notify) {
if !watchIPNArgs.netmap {
n.NetMap = nil
}
j, _ := json.MarshalIndent(n, "", "\t")
printf("%s\n", j)
})
bc.RequestEngineStatus()
pump(ctx, bc, c)
return errors.New("exit")
}
func runDERPMap(ctx context.Context, args []string) error {
dm, err := tailscale.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
)
}
enc := json.NewEncoder(Stdout)
enc.SetIndent("", "\t")
enc.Encode(dm)
return nil
}
func runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)
}
return nil
}
func runDaemonGoroutines(ctx context.Context, args []string) error {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {
return err
}
Stdout.Write(goroutines)
return nil
}
var metricsArgs struct {
watch bool
}
func runDaemonMetrics(ctx context.Context, args []string) error {
last := map[string]int64{}
for {
out, err := tailscale.DaemonMetrics(ctx)
if err != nil {
return err
}
if !metricsArgs.watch {
Stdout.Write(out)
return nil
}
bs := bufio.NewScanner(bytes.NewReader(out))
type change struct {
name string
from, to int64
}
var changes []change
var maxNameLen int
for bs.Scan() {
line := bytes.TrimSpace(bs.Bytes())
if len(line) == 0 || line[0] == '#' {
continue
}
f := strings.Fields(string(line))
if len(f) != 2 {
continue
}
name := f[0]
n, _ := strconv.ParseInt(f[1], 10, 64)
prev, ok := last[name]
if ok && prev == n {
continue
}
last[name] = n
if !ok {
continue
}
changes = append(changes, change{name, prev, n})
if len(name) > maxNameLen {
maxNameLen = len(name)
}
}
if len(changes) > 0 {
format := fmt.Sprintf("%%-%ds %%+5d => %%v\n", maxNameLen)
for _, c := range changes {
fmt.Fprintf(Stdout, format, c.name, c.to-c.from, c.to)
}
io.WriteString(Stdout, "\n")
}
time.Sleep(time.Second)
}
}

View File

@@ -25,23 +25,23 @@ func fixTailscaledConnectError(origErr error) error {
if err != nil {
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
}
found := false
var foundProc ps.Process
for _, proc := range procs {
base := filepath.Base(proc.Executable())
if base == "tailscaled" {
found = true
foundProc = proc
break
}
if runtime.GOOS == "darwin" && base == "IPNExtension" {
found = true
foundProc = proc
break
}
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
found = true
foundProc = proc
break
}
}
if !found {
if foundProc == nil {
switch runtime.GOOS {
case "windows":
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
@@ -52,5 +52,5 @@ func fixTailscaledConnectError(origErr error) error {
}
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
}
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
}

View File

@@ -11,11 +11,9 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
@@ -30,6 +28,7 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
@@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error {
return err
}
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
stableID, isOffline, err := getTargetStableID(ctx, ip)
if err != nil {
return fmt.Errorf("can't send to %s: %v", target, err)
}
@@ -154,32 +153,21 @@ func runCp(ctx context.Context, args []string) error {
}
}
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
if err != nil {
return err
}
req.ContentLength = contentLength
if cpArgs.verbose {
log.Printf("sending to %v ...", dstURL)
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
res, err := http.DefaultClient.Do(req)
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
continue
if cpArgs.verbose {
log.Printf("sent %q", name)
}
io.Copy(Stdout, res.Body)
res.Body.Close()
return errors.New(res.Status)
}
return nil
}
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", false, err
@@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
continue
}
isOffline = n.Online != nil && !*n.Online
return ft.PeerAPIURL, isOffline, nil
return n.StableID, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)

View File

@@ -18,12 +18,13 @@ import (
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-4] [-6] [peername]",
ShortHelp: "Show current Tailscale IP address(es)",
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ip")
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
return fs
@@ -31,13 +32,14 @@ var ipCmd = &ffcli.Command{
}
var ipArgs struct {
want1 bool
want4 bool
want6 bool
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 1 {
return errors.New("unknown arguments")
return errors.New("too many arguments, expected at most one peer")
}
var of string
if len(args) == 1 {
@@ -45,8 +47,14 @@ func runIP(ctx context.Context, args []string) error {
}
v4, v6 := ipArgs.want4, ipArgs.want6
if v4 && v6 {
return errors.New("tailscale ip -4 and -6 are mutually exclusive")
nflags := 0
for _, b := range []bool{ipArgs.want1, v4, v6} {
if b {
nflags++
}
}
if nflags > 1 {
return errors.New("tailscale ip -1, -4, and -6 are mutually exclusive")
}
if !v4 && !v6 {
v4, v6 = true, true
@@ -71,6 +79,9 @@ func runIP(ctx context.Context, args []string) error {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
if ipArgs.want1 {
ips = ips[:1]
}
match := false
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {

View File

@@ -29,7 +29,22 @@ var statusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status [--active] [--web] [--json]",
ShortHelp: "Show state of tailscaled and its connections",
Exec: runStatus,
LongHelp: strings.TrimSpace(`
JSON FORMAT
Warning: this format has changed between releases and might change more
in the future.
For a description of the fields, see the "type Status" declaration at:
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
(and be sure to select branch/tag that corresponds to the version
of Tailscale you're running)
`),
Exec: runStatus,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("status")
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
@@ -145,11 +160,19 @@ func runStatus(ctx context.Context, args []string) error {
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
var offline string
if !ps.Online {
offline = "; offline"
}
if !ps.Active {
if ps.ExitNode {
f("idle; exit node")
f("idle; exit node" + offline)
} else if ps.ExitNodeOption {
f("idle; offers exit node" + offline)
} else if anyTraffic {
f("idle")
f("idle" + offline)
} else if !ps.Online {
f("offline")
} else {
f("-")
}
@@ -157,12 +180,17 @@ func runStatus(ctx context.Context, args []string) error {
f("active; ")
if ps.ExitNode {
f("exit node; ")
} else if ps.ExitNodeOption {
f("offers exit node; ")
}
if relay != "" && ps.CurAddr == "" {
f("relay %q", relay)
} else if ps.CurAddr != "" {
f("direct %s", ps.CurAddr)
}
if !ps.Online {
f("; offline")
}
}
if anyTraffic {
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)

View File

@@ -6,6 +6,8 @@ package cli
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -28,6 +30,8 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/dnsname"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -46,8 +50,10 @@ down").
If flags are specified, the flags must be the complete set of desired
settings. An error is returned if any setting would be changed as a
result of an unspecified flag's default value, unless the --reset
flag is also used.
result of an unspecified flag's default value, unless the --reset flag
is also used. (The flags --authkey, --force-reauth, and --qr are not
considered settings that need to be re-specified when modifying
settings.)
`),
FlagSet: upFlagSet,
Exec: runUp,
@@ -60,20 +66,34 @@ func effectiveGOOS() string {
return runtime.GOOS
}
// acceptRouteDefault returns the CLI's default value of --accept-routes as
// a function of the platform it's running on.
func acceptRouteDefault(goos string) bool {
switch goos {
case "windows":
return true
case "darwin":
return version.IsSandboxedMacOS()
default:
return false
}
}
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf := newFlagSet("up")
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
@@ -121,6 +141,7 @@ type upArgsT struct {
authKeyOrFile string // "secret" or "file:/path/to/secret"
hostname string
opUser string
json bool
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -138,6 +159,33 @@ func (a upArgsT) getAuthKey() (string, error) {
var upArgs upArgsT
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
//
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
// providing the link for where to authenticate this client. BackendState would be
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
// populated if something goes badly wrong.
//
// When the client is authenticated by having someone visit the AuthURL, a second
// JSON block will be output. The AuthURL and QR fields will not be present, the
// BackendState and Error fields will give the result of the authentication.
// Ex:
// {
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
// "QR": "data:image/png;base64,0123...cdef"
// "BackendState": "NeedsLogin"
// }
// {
// "BackendState": "Running"
// }
//
type upOutputJSON struct {
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
Error string `json:",omitempty"` // description of an error
}
func warnf(format string, args ...interface{}) {
printf("Warning: "+format+"\n", args...)
}
@@ -190,6 +238,65 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
return routes, nil
}
// peerWithTailscaleIP returns the peer in st with the provided
// Tailscale IP.
func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) {
for _, ps := range st.Peer {
for _, ip2 := range ps.TailscaleIPs {
if ip == ip2 {
return ps, true
}
}
}
return nil, false
}
// exitNodeIPOfArg maps from a user-provided CLI flag value to an IP
// address they want to use as an exit node.
func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) {
if arg == "" {
return ip, errors.New("invalid use of exitNodeIPOfArg with empty string")
}
ip, err = netaddr.ParseIP(arg)
if err == nil {
// If we're online already and have a netmap, double check that the IP
// address specified is valid.
if st.BackendState == "Running" {
ps, ok := peerWithTailscaleIP(st, ip)
if !ok {
return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
}
}
return ip, err
}
match := 0
for _, ps := range st.Peer {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if !strings.EqualFold(arg, baseName) {
continue
}
match++
if len(ps.TailscaleIPs) == 0 {
return ip, fmt.Errorf("node %q has no Tailscale IP?", arg)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %q is not advertising an exit node", arg)
}
ip = ps.TailscaleIPs[0]
}
switch match {
case 0:
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", arg)
case 1:
return ip, nil
default:
return ip, fmt.Errorf("ambiguous exit node name %q", arg)
}
}
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
//
// Note that the parameters upArgs and warnf are named intentionally
@@ -205,9 +312,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
var exitNodeIP netaddr.IP
if upArgs.exitNodeIP != "" {
var err error
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st)
if err != nil {
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
return nil, err
}
} else if upArgs.exitNodeAllowLANAccess {
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
@@ -380,11 +487,12 @@ func runUp(ctx context.Context, args []string) error {
env := upCheckEnv{
goos: effectiveGOOS(),
distro: distro.Get(),
user: os.Getenv("USER"),
flagSet: upFlagSet,
upArgs: upArgs,
backendState: st.BackendState,
curExitNodeIP: exitNodeIP(prefs, st),
curExitNodeIP: exitNodeIP(curPrefs, st),
}
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil {
@@ -435,10 +543,16 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive()
case ipn.NeedsMachineAuth:
printed = true
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
if env.upArgs.json {
printUpDoneJSON(ipn.NeedsMachineAuth, "")
} else {
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
case ipn.Running:
// Done full authentication process
if printed {
if env.upArgs.json {
printUpDoneJSON(ipn.Running, "")
} else if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(Stderr, "Success.\n")
}
@@ -451,15 +565,33 @@ func runUp(ctx context.Context, args []string) error {
}
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
printed = true
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
if upArgs.json {
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
q, err := qrcode.New(*url, qrcode.Medium)
if err != nil {
log.Printf("QR code error: %v", err)
} else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
if err == nil {
png, err := q.PNG(128)
if err == nil {
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
}
}
data, err := json.MarshalIndent(js, "", "\t")
if err != nil {
log.Printf("upOutputJSON marshalling error: %v", err)
} else {
fmt.Println(string(data))
}
} else {
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium)
if err != nil {
log.Printf("QR code error: %v", err)
} else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
}
}
}
}
})
@@ -546,6 +678,16 @@ func runUp(ctx context.Context, args []string) error {
}
}
func printUpDoneJSON(state ipn.State, errorString string) {
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
data, err := json.MarshalIndent(js, "", " ")
if err != nil {
log.Printf("printUpDoneJSON marshalling error: %v", err)
} else {
fmt.Println(string(data))
}
}
var (
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
)
@@ -588,7 +730,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref.
func preflessFlag(flagName string) bool {
switch flagName {
case "authkey", "force-reauth", "reset", "qr":
case "authkey", "force-reauth", "reset", "qr", "json":
return true
}
return false
@@ -622,6 +764,7 @@ type upCheckEnv struct {
upArgs upArgsT
backendState string
curExitNodeIP netaddr.IP
distro distro.Distro
}
// checkForAccidentalSettingReverts (the "up checker") checks for
@@ -672,6 +815,10 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
continue
}
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
continue
}
missing = append(missing, fmtFlagValueArg(flagName, valCur))
}
if len(missing) == 0 {

View File

@@ -3,6 +3,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
@@ -72,6 +73,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
@@ -90,7 +92,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/dns/dnsmessage from net
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
@@ -102,7 +104,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+

View File

@@ -25,7 +25,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
@@ -59,10 +60,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
github.com/go-multierror/multierror from tailscale.com/cmd/tailscaled+
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
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
@@ -92,35 +93,38 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
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+
L 💣 github.com/vishvananda/netlink from tailscale.com/wgengine/router
L 💣 github.com/vishvananda/netlink/nl from github.com/vishvananda/netlink
L github.com/vishvananda/netns from github.com/vishvananda/netlink+
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
W 💣 golang.zx2c4.com/wireguard/ipc/winpipe from golang.zx2c4.com/wireguard/ipc
W 💣 golang.zx2c4.com/wireguard/ipc/namedpipe from golang.zx2c4.com/wireguard/ipc
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
inet.af/netaddr from inet.af/wf+
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
inet.af/netstack/context from inet.af/netstack/refs+
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
inet.af/netstack/linewriter from inet.af/netstack/log
inet.af/netstack/log from inet.af/netstack/state+
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
inet.af/netstack/refs from inet.af/netstack/refsvfs2
inet.af/netstack/refsvfs2 from inet.af/netstack/tcpip/stack
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
💣 inet.af/netstack/state from inet.af/netstack/atomicbitops+
inet.af/netstack/state/wire from inet.af/netstack/state
@@ -131,6 +135,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/internal/tcp from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
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+
@@ -140,7 +145,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/tcpip/transport from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/transport/internal/network from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/internal/noop from inet.af/netstack/tcpip/transport/raw
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
@@ -173,30 +181,31 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
tailscale.com/kube from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
W tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logtail from tailscale.com/logpolicy
tailscale.com/logtail from tailscale.com/logpolicy+
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/filch from tailscale.com/logpolicy
💣 tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns/resolver from tailscale.com/net/dns+
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netknob from tailscale.com/ipn/localapi+
tailscale.com/net/netknob from tailscale.com/logpolicy+
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
tailscale.com/net/socks5/tssocks from tailscale.com/cmd/tailscaled
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/paths from tailscale.com/client/tailscale+
@@ -218,16 +227,18 @@ 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/derp+
tailscale.com/types/pad32 from tailscale.com/derp
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+
tailscale.com/util/clientmetric from tailscale.com/ipn/localapi+
L tailscale.com/util/cmpver from tailscale.com/net/dns
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
LW tailscale.com/util/endian from tailscale.com/net/dns+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
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
@@ -242,7 +253,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
@@ -265,7 +276,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
golang.org/x/net/http2/hpack from net/http+
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
@@ -356,7 +369,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
regexp/syntax from regexp
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+

View File

@@ -20,6 +20,7 @@ import (
"net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
@@ -27,17 +28,23 @@ import (
"syscall"
"time"
"github.com/go-multierror/multierror"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/netns"
"tailscale.com/net/socks5/tssocks"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -78,6 +85,7 @@ var args struct {
debug string
port uint16
statepath string
statedir string
socketpath string
birdSocketPath string
verbose int
@@ -114,7 +122,8 @@ func main() {
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
@@ -169,8 +178,7 @@ func main() {
osshare.SetFileSharingEnabled(false, logger.Discard)
if err != nil {
// No need to log; the func already did
os.Exit(1)
log.Fatal(err)
}
}
@@ -202,6 +210,16 @@ func trySynologyMigration(p string) error {
return nil
}
func statePathOrDefault() string {
if args.statepath != "" {
return args.statepath
}
if args.statedir != "" {
return filepath.Join(args.statedir, "tailscaled.state")
}
return ""
}
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.
@@ -210,9 +228,15 @@ func ipnServerOpts() (o ipnserver.Options) {
goos = runtime.GOOS
}
o.Port = 41112
o.StatePath = args.statepath
o.SocketPath = args.socketpath // even for goos=="windows", for tests
o.VarRoot = args.statedir
// If an absolute --state is provided but not --statedir, try to derive
// a state directory.
if o.VarRoot == "" && filepath.IsAbs(args.statepath) {
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") {
o.VarRoot = dir
}
}
switch goos {
default:
@@ -227,7 +251,7 @@ func ipnServerOpts() (o ipnserver.Options) {
func run() error {
var err error
pol := logpolicy.New("tailnode.log.tailscale.io")
pol := logpolicy.New(logtail.CollectionNode)
pol.SetVerbosityLevel(args.verbose)
defer func() {
// Finish uploading logs after closing everything else.
@@ -261,10 +285,10 @@ func run() error {
return nil
}
if args.statepath == "" {
log.Fatalf("--state is required")
if args.statepath == "" && args.statedir == "" {
log.Fatalf("--statedir (or at least --state) is required")
}
if err := trySynologyMigration(args.statepath); err != nil {
if err := trySynologyMigration(statePathOrDefault()); err != nil {
log.Printf("error in synology migration: %v", err)
}
@@ -276,36 +300,55 @@ func run() error {
linkMon, err := monitor.New(logf)
if err != nil {
log.Fatalf("creating link monitor: %v", err)
return fmt.Errorf("monitor.New: %w", err)
}
pol.Logtail.SetLinkMonitor(linkMon)
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
e, useNetstack, err := createEngine(logf, linkMon)
dialer := new(tsdial.Dialer) // mutated below (before used)
e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
logf("wgengine.New: %v", err)
return err
return fmt.Errorf("createEngine: %w", err)
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
var ns *netstack.Impl
if useNetstack || wrapNetstack {
onlySubnets := wrapNetstack && !useNetstack
ns = mustStartNetstack(logf, e, onlySubnets)
ns, err := newNetstack(logf, dialer, e)
if err != nil {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
if err := ns.Start(); err != nil {
return fmt.Errorf("failed to start netstack: %w", err)
}
if useNetstack {
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := e.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
}
if socksListener != nil || httpProxyListener != nil {
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
}
if socksListener != nil {
ss := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
Dialer: dialer.UserDial,
}
go func() {
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
}()
}
}
@@ -332,32 +375,49 @@ func run() error {
}()
opts := ipnServerOpts()
opts.DebugMux = debugMux
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
return fmt.Errorf("ipnserver.StateStore: %w", err)
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
if err != nil {
return fmt.Errorf("ipnserver.New: %w", err)
}
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = srv.Run(ctx, ln)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
logf("ipnserver.Run: %v", err)
return err
return fmt.Errorf("ipnserver.Run: %w", err)
}
return nil
}
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
if args.tunname == "" {
return nil, false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
e, useNetstack, err = tryEngine(logf, linkMon, name)
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
if err == nil {
return e, useNetstack, nil
}
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
errs = append(errs, err)
}
return nil, false, multierror.New(errs)
return nil, false, multierr.New(errs...)
}
var wrapNetstack = shouldWrapNetstack()
@@ -382,10 +442,11 @@ func shouldWrapNetstack() bool {
return false
}
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
}
useNetstack = name == "userspace-networking"
@@ -395,14 +456,14 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
}
}
if !useNetstack {
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
return nil, false, err
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev
if strings.HasPrefix(name, "tap:") {
@@ -414,11 +475,11 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, err
return nil, false, fmt.Errorf("creating router: %w", err)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
conf.DNS = d
conf.Router = r
@@ -435,6 +496,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
func newDebugMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/debug/metrics", servePrometheusMetrics)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
@@ -443,6 +505,11 @@ func newDebugMux() *http.ServeMux {
return mux
}
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
func runDebugServer(mux *http.ServeMux, addr string) {
srv := &http.Server{
Addr: addr,
@@ -453,33 +520,54 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
ns, err := netstack.Create(logf, tunDev, e, magicConn, onlySubnets)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
return ns
return netstack.Create(logf, tunDev, e, magicConn, dialer)
}
func mustStartTCPListener(name, addr string) net.Listener {
if addr == "" {
return nil
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
// proxies, if the respective addresses are not empty. socksAddr and
// httpAddr can be the same, in which case socksListener will receive
// connections that look like they're speaking SOCKS and httpListener
// will receive everything else.
//
// socksListener and httpListener can be nil, if their respective
// addrs are empty.
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
ln, err := net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("proxy listener: %v", err)
}
return proxymux.SplitSOCKSAndHTTP(ln)
}
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("%v listener: %v", name, err)
var err error
if socksAddr != "" {
socksListener, err = net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("SOCKS5 listener: %v", err)
}
if strings.HasSuffix(socksAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
}
}
if strings.HasSuffix(addr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("%v listening on %v", name, ln.Addr())
if httpAddr != "" {
httpListener, err = net.Listen("tcp", httpAddr)
if err != nil {
log.Fatalf("HTTP proxy listener: %v", err)
}
if strings.HasSuffix(httpAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
}
}
return ln
return socksListener, httpListener
}

View File

@@ -32,12 +32,15 @@ import (
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/safesocket"
"tailscale.com/types/logger"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -74,7 +77,14 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
go func() {
defer close(doneCh)
args := []string{"/subproc", service.Policy.PublicID.String()}
ipnserver.BabysitProc(ctx, args, log.Printf)
// Make a logger without a date prefix, as filelogger
// and logtail both already add their own. All we really want
// from the log package is the automatic newline.
// We start with log.Default().Writer(), which is the logtail
// writer that logpolicy already installed as the global
// output.
logger := log.New(log.Default().Writer(), "", 0)
ipnserver.BabysitProc(ctx, args, logger.Printf)
}()
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
@@ -109,6 +119,9 @@ func beWindowsSubprocess() bool {
}
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file logggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
@@ -172,6 +185,12 @@ func beFirewallKillswitch() bool {
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
linkMon, err := monitor.New(logf)
if err != nil {
return err
}
dialer := new(tsdial.Dialer)
getEngineRaw := func() (wgengine.Engine, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
@@ -192,19 +211,26 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, fmt.Errorf("DNS: %w", err)
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
LinkMonitor: linkMon,
Dialer: dialer,
})
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("engine: %w", err)
}
onlySubnets := true
if wrapNetstack {
mustStartNetstack(logf, eng, onlySubnets)
ns, err := newNetstack(logf, dialer, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
if err := ns.Start(); err != nil {
return nil, fmt.Errorf("failed to start netstack: %w", err)
}
return wgengine.NewWatchdog(eng), nil
}
@@ -266,7 +292,18 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
}
}
err := ipnserver.Run(ctx, logf, logid, getEngine, ipnServerOpts())
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
return err
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
if err != nil {
logf("ipnserver.Run: %v", err)
}

View File

@@ -157,8 +157,9 @@ func handleSSH(s ssh.Session) {
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
} else {
s.Exit(0)
}
s.Exit(0)
return
}

View File

@@ -339,11 +339,9 @@ func (c *Auto) authRoutine() {
continue
}
if url != "" {
if goal.url != "" {
err = fmt.Errorf("[unexpected] server required a new URL?")
report(err, "WaitLoginURL")
}
// goal.url ought to be empty here.
// However, not all control servers get this right,
// and logging about it here just generates noise.
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: true,

View File

@@ -79,3 +79,9 @@ type Client interface {
// requesting a DNS record be created or updated.
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
}
// UserVisibleError is an error that should be shown to users.
type UserVisibleError string
func (e UserVisibleError) Error() string { return string(e) }
func (e UserVisibleError) UserVisibleError() string { return string(e) }

View File

@@ -46,6 +46,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/util/systemd"
"tailscale.com/wgengine/monitor"
)
@@ -60,7 +61,7 @@ type Direct struct {
keepAlive bool
logf logger.Logf
linkMon *monitor.Mon // or nil
discoPubKey tailcfg.DiscoKey
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
keepSharerAndUserSplit bool
@@ -88,7 +89,7 @@ type Options struct {
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
DiscoPublicKey key.DiscoPublic
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
@@ -146,13 +147,20 @@ func NewDirect(opts Options) (*Direct, error) {
}
httpc := opts.HTTPTestClient
if httpc == nil && runtime.GOOS == "js" {
// In js/wasm, net/http.Transport (as of Go 1.18) will
// only use the browser's Fetch API if you're using
// the DefaultClient (or a client without dial hooks
// etc set).
httpc = http.DefaultClient
}
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
}
dialer := netns.NewDialer()
dialer := netns.NewDialer(opts.Logf)
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
@@ -284,8 +292,8 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
tryingNewKey := c.tryingNewKey
serverKey := c.serverKey
authKey := c.authKey
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
hi := c.hostinfo.Clone()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
@@ -357,9 +365,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
OldNodeKey: tailcfg.NodeKeyFromNodePublic(oldNodeKey),
NodeKey: tailcfg.NodeKeyFromNodePublic(tryingNewKey.Public()),
Hostinfo: hostinfo,
OldNodeKey: oldNodeKey,
NodeKey: tryingNewKey.Public(),
Hostinfo: hi,
Followup: opt.URL,
Timestamp: &now,
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
@@ -431,7 +439,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
if resp.Error != "" {
return false, "", errors.New(resp.Error)
return false, "", UserVisibleError(resp.Error)
}
if resp.NodeKeyExpired {
if regen {
@@ -551,12 +559,21 @@ const pollTimeout = 120 * time.Second
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
metricMapRequests.Add(1)
metricMapRequestsActive.Add(1)
defer metricMapRequestsActive.Add(-1)
if maxPolls == -1 {
metricMapRequestsPoll.Add(1)
} else {
metricMapRequestsLite.Add(1)
}
c.mu.Lock()
persist := c.persist
serverURL := c.serverURL
serverKey := c.serverKey
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
hi := c.hostinfo.Clone()
backendLogID := hi.BackendLogID
localPort := c.localPort
var epStrs []string
var epTypes []tailcfg.EndpointType
@@ -595,18 +612,18 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
request := &tailcfg.MapRequest{
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKeyFromNodePublic(persist.PrivateNodeKey.Public()),
NodeKey: persist.PrivateNodeKey.Public(),
DiscoKey: c.discoPubKey,
Endpoints: epStrs,
EndpointTypes: epTypes,
Stream: allowStream,
Hostinfo: hostinfo,
Hostinfo: hi,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
@@ -615,6 +632,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
if health.NetworkCategoryHealth() != nil {
extraDebugFlags = append(extraDebugFlags, "warn-network-category-unhealthy")
}
if hostinfo.DisabledEtcAptSource() {
extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled")
}
if len(extraDebugFlags) > 0 {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
@@ -737,11 +757,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return err
}
metricMapResponseMessages.Add(1)
if allowStream {
health.GotStreamedMapResponse()
}
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
metricMapResponsePings.Add(1)
go answerPing(c.logf, c.httpc, pr)
}
@@ -758,13 +781,23 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return ctx.Err()
}
if resp.KeepAlive {
metricMapResponseKeepAlives.Add(1)
continue
}
metricMapResponseMap.Add(1)
if i > 0 {
metricMapResponseMapDelta.Add(1)
}
hasDebug := resp.Debug != nil
// being conservative here, if Debug not present set to False
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
if hasDebug {
if code := resp.Debug.Exit; code != nil {
c.logf("exiting process with status %v per controlplane", *code)
os.Exit(*code)
}
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
@@ -1167,7 +1200,13 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
metricSetDNS.Add(1)
defer func() {
if err != nil {
metricSetDNSError.Add(1)
}
}()
c.mu.Lock()
serverKey := c.serverKey
c.mu.Unlock()
@@ -1267,3 +1306,20 @@ func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg
}
return nil
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
metricMapRequests = clientmetric.NewCounter("controlclient_map_requests")
metricMapRequestsLite = clientmetric.NewCounter("controlclient_map_requests_lite")
metricMapRequestsPoll = clientmetric.NewCounter("controlclient_map_requests_poll")
metricMapResponseMessages = clientmetric.NewCounter("controlclient_map_response_message") // any message type
metricMapResponsePings = clientmetric.NewCounter("controlclient_map_response_ping")
metricMapResponseKeepAlives = clientmetric.NewCounter("controlclient_map_response_keepalive")
metricMapResponseMap = clientmetric.NewCounter("controlclient_map_response_map") // any non-keepalive map response
metricMapResponseMapDelta = clientmetric.NewCounter("controlclient_map_response_map_delta") // 2nd+ non-keepalive map response
metricSetDNS = clientmetric.NewCounter("controlclient_setdns")
metricSetDNSError = clientmetric.NewCounter("controlclient_setdns_error")
)

View File

@@ -110,7 +110,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
nm := &netmap.NetworkMap{
NodeKey: tailcfg.NodeKeyFromNodePublic(ms.privateNodeKey.Public()),
NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,

359
control/noise/conn.go Normal file
View File

@@ -0,0 +1,359 @@
// 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 noise implements the base transport of the Tailscale 2021
// control protocol.
//
// The base transport implements Noise IK, instantiated with
// Curve25519, ChaCha20Poly1305 and BLAKE2s.
package noise
import (
"crypto/cipher"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"golang.org/x/crypto/blake2s"
chp "golang.org/x/crypto/chacha20poly1305"
"tailscale.com/types/key"
)
const (
// maxMessageSize is the maximum size of a protocol frame on the
// wire, including header and payload.
maxMessageSize = 4096
// maxCiphertextSize is the maximum amount of ciphertext bytes
// that one protocol frame can carry, after framing.
maxCiphertextSize = maxMessageSize - 3
// maxPlaintextSize is the maximum amount of plaintext bytes that
// one protocol frame can carry, after encryption and framing.
maxPlaintextSize = maxCiphertextSize - chp.Overhead
)
// A Conn is a secured Noise connection. It implements the net.Conn
// interface, with the unusual trait that any write error (including a
// SetWriteDeadline induced i/o timeout) causes all future writes to
// fail.
type Conn struct {
conn net.Conn
version uint16
peer key.MachinePublic
handshakeHash [blake2s.Size]byte
rx rxState
tx txState
}
// rxState is all the Conn state that Read uses.
type rxState struct {
sync.Mutex
cipher cipher.AEAD
nonce nonce
buf [maxMessageSize]byte
n int // number of valid bytes in buf
next int // offset of next undecrypted packet
plaintext []byte // slice into buf of decrypted bytes
}
// txState is all the Conn state that Write uses.
type txState struct {
sync.Mutex
cipher cipher.AEAD
nonce nonce
buf [maxMessageSize]byte
err error // records the first partial write error for all future calls
}
// ProtocolVersion returns the protocol version that was used to
// establish this Conn.
func (c *Conn) ProtocolVersion() int {
return int(c.version)
}
// HandshakeHash returns the Noise handshake hash for the connection,
// which can be used to bind other messages to this connection
// (i.e. to ensure that the message wasn't replayed from a different
// connection).
func (c *Conn) HandshakeHash() [blake2s.Size]byte {
return c.handshakeHash
}
// Peer returns the peer's long-term public key.
func (c *Conn) Peer() key.MachinePublic {
return c.peer
}
// readNLocked reads into c.rx.buf until buf contains at least total
// bytes. Returns a slice of the total bytes in rxBuf, or an
// error if fewer than total bytes are available.
func (c *Conn) readNLocked(total int) ([]byte, error) {
if total > maxMessageSize {
return nil, errReadTooBig{total}
}
for {
if total <= c.rx.n {
return c.rx.buf[:total], nil
}
n, err := c.conn.Read(c.rx.buf[c.rx.n:])
c.rx.n += n
if err != nil {
return nil, err
}
}
}
// decryptLocked decrypts msg (which is header+ciphertext) in-place
// and sets c.rx.plaintext to the decrypted bytes.
func (c *Conn) decryptLocked(msg []byte) (err error) {
if msgType := msg[0]; msgType != msgTypeRecord {
return fmt.Errorf("received message with unexpected type %d, want %d", msgType, msgTypeRecord)
}
// We don't check the length field here, because the caller
// already did in order to figure out how big the msg slice should
// be.
ciphertext := msg[headerLen:]
if !c.rx.nonce.Valid() {
return errCipherExhausted{}
}
c.rx.plaintext, err = c.rx.cipher.Open(ciphertext[:0], c.rx.nonce[:], ciphertext, nil)
c.rx.nonce.Increment()
if err != nil {
// Once a decryption has failed, our Conn is no longer
// synchronized with our peer. Nuke the cipher state to be
// safe, so that no further decryptions are attempted. Future
// read attempts will return net.ErrClosed.
c.rx.cipher = nil
}
return err
}
// encryptLocked encrypts plaintext into c.tx.buf (including the
// packet header) and returns a slice of the ciphertext, or an error
// if the cipher is exhausted (i.e. can no longer be used safely).
func (c *Conn) encryptLocked(plaintext []byte) ([]byte, error) {
if !c.tx.nonce.Valid() {
// Received 2^64-1 messages on this cipher state. Connection
// is no longer usable.
return nil, errCipherExhausted{}
}
c.tx.buf[0] = msgTypeRecord
binary.BigEndian.PutUint16(c.tx.buf[1:headerLen], uint16(len(plaintext)+chp.Overhead))
ret := c.tx.cipher.Seal(c.tx.buf[:headerLen], c.tx.nonce[:], plaintext, nil)
c.tx.nonce.Increment()
return ret, nil
}
// wholeMessageLocked returns a slice of one whole Noise transport
// message from c.rx.buf, if one whole message is available, and
// advances the read state to the next Noise message in the
// buffer. Returns nil without advancing read state if there isn't one
// whole message in c.rx.buf.
func (c *Conn) wholeMessageLocked() []byte {
available := c.rx.n - c.rx.next
if available < headerLen {
return nil
}
bs := c.rx.buf[c.rx.next:c.rx.n]
totalSize := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
if len(bs) < totalSize {
return nil
}
c.rx.next += totalSize
return bs[:totalSize]
}
// decryptOneLocked decrypts one Noise transport message, reading from
// c.conn as needed, and sets c.rx.plaintext to point to the decrypted
// bytes. c.rx.plaintext is only valid if err == nil.
func (c *Conn) decryptOneLocked() error {
c.rx.plaintext = nil
// Fast path: do we have one whole ciphertext frame buffered
// already?
if bs := c.wholeMessageLocked(); bs != nil {
return c.decryptLocked(bs)
}
if c.rx.next != 0 {
// To simplify the read logic, move the remainder of the
// buffered bytes back to the head of the buffer, so we can
// grow it without worrying about wraparound.
c.rx.n = copy(c.rx.buf[:], c.rx.buf[c.rx.next:c.rx.n])
c.rx.next = 0
}
bs, err := c.readNLocked(headerLen)
if err != nil {
return err
}
// The rest of the header (besides the length field) gets verified
// in decryptLocked, not here.
messageLen := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
bs, err = c.readNLocked(messageLen)
if err != nil {
return err
}
c.rx.next = len(bs)
return c.decryptLocked(bs)
}
// Read implements io.Reader.
func (c *Conn) Read(bs []byte) (int, error) {
c.rx.Lock()
defer c.rx.Unlock()
if c.rx.cipher == nil {
return 0, net.ErrClosed
}
// If no plaintext is buffered, decrypt incoming frames until we
// have some plaintext. Zero-byte Noise frames are allowed in this
// protocol, which is why we have to loop here rather than decrypt
// a single additional frame.
for len(c.rx.plaintext) == 0 {
if err := c.decryptOneLocked(); err != nil {
return 0, err
}
}
n := copy(bs, c.rx.plaintext)
c.rx.plaintext = c.rx.plaintext[n:]
return n, nil
}
// Write implements io.Writer.
func (c *Conn) Write(bs []byte) (n int, err error) {
c.tx.Lock()
defer c.tx.Unlock()
if c.tx.err != nil {
return 0, c.tx.err
}
defer func() {
if err != nil {
// All write errors are fatal for this conn, so clear the
// cipher state whenever an error happens.
c.tx.cipher = nil
}
if c.tx.err == nil {
// Only set c.tx.err if not nil so that we can return one
// error on the first failure, and a different one for
// subsequent calls. See the error handling around Write
// below for why.
c.tx.err = err
}
}()
if c.tx.cipher == nil {
return 0, net.ErrClosed
}
var sent int
for len(bs) > 0 {
toSend := bs
if len(toSend) > maxPlaintextSize {
toSend = bs[:maxPlaintextSize]
}
bs = bs[len(toSend):]
ciphertext, err := c.encryptLocked(toSend)
if err != nil {
return 0, err
}
n, err := c.conn.Write(ciphertext)
sent += n
if err != nil {
// Return the raw error on the Write that actually
// failed. For future writes, return that error wrapped in
// a desync error.
c.tx.err = errPartialWrite{err}
return sent, err
}
}
return sent, nil
}
// Close implements io.Closer.
func (c *Conn) Close() error {
closeErr := c.conn.Close() // unblocks any waiting reads or writes
// Remove references to live cipher state. Strictly speaking this
// is unnecessary, but we want to try and hand the active cipher
// state to the garbage collector promptly, to preserve perfect
// forward secrecy as much as we can.
c.rx.Lock()
c.rx.cipher = nil
c.rx.Unlock()
c.tx.Lock()
c.tx.cipher = nil
c.tx.Unlock()
return closeErr
}
func (c *Conn) LocalAddr() net.Addr { return c.conn.LocalAddr() }
func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() }
func (c *Conn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) }
func (c *Conn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) }
func (c *Conn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) }
// errCipherExhausted is the error returned when we run out of nonces
// on a cipher.
type errCipherExhausted struct{}
func (errCipherExhausted) Error() string {
return "cipher exhausted, no more nonces available for current key"
}
func (errCipherExhausted) Timeout() bool { return false }
func (errCipherExhausted) Temporary() bool { return false }
// errPartialWrite is the error returned when the cipher state has
// become unusable due to a past partial write.
type errPartialWrite struct {
err error
}
func (e errPartialWrite) Error() string {
return fmt.Sprintf("cipher state desynchronized due to partial write (%v)", e.err)
}
func (e errPartialWrite) Unwrap() error { return e.err }
func (e errPartialWrite) Temporary() bool { return false }
func (e errPartialWrite) Timeout() bool { return false }
// errReadTooBig is the error returned when the peer sent an
// unacceptably large Noise frame.
type errReadTooBig struct {
requested int
}
func (e errReadTooBig) Error() string {
return fmt.Sprintf("requested read of %d bytes exceeds max allowed Noise frame size", e.requested)
}
func (e errReadTooBig) Temporary() bool {
// permanent error because this error only occurs when our peer
// sends us a frame so large we're unwilling to ever decode it.
return false
}
func (e errReadTooBig) Timeout() bool { return false }
type nonce [chp.NonceSize]byte
func (n *nonce) Valid() bool {
return binary.BigEndian.Uint32(n[:4]) == 0 && binary.BigEndian.Uint64(n[4:]) != invalidNonce
}
func (n *nonce) Increment() {
if !n.Valid() {
panic("increment of invalid nonce")
}
binary.BigEndian.PutUint64(n[4:], 1+binary.BigEndian.Uint64(n[4:]))
}

339
control/noise/conn_test.go Normal file
View File

@@ -0,0 +1,339 @@
// 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 noise
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"net"
"strings"
"sync"
"testing"
"testing/iotest"
chp "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/nettest"
tsnettest "tailscale.com/net/nettest"
"tailscale.com/types/key"
)
func TestMessageSize(t *testing.T) {
// This test is a regression guard against someone looking at
// maxCiphertextSize, going "huh, we could be more efficient if it
// were larger, and accidentally violating the Noise spec. Do not
// change this max value, it's a deliberate limitation of the
// cryptographic protocol we use (see Section 3 "Message Format"
// of the Noise spec).
const max = 65535
if maxCiphertextSize > max {
t.Fatalf("max ciphertext size is %d, which is larger than the maximum noise message size %d", maxCiphertextSize, max)
}
}
func TestConnBasic(t *testing.T) {
client, server := pair(t)
sb := sinkReads(server)
want := "test"
if _, err := io.WriteString(client, want); err != nil {
t.Fatalf("client write failed: %v", err)
}
client.Close()
if got := sb.String(4); got != want {
t.Fatalf("wrong content received: got %q, want %q", got, want)
}
if err := sb.Error(); err != io.EOF {
t.Fatal("client close wasn't seen by server")
}
if sb.Total() != 4 {
t.Fatalf("wrong amount of bytes received: got %d, want 4", sb.Total())
}
}
// bufferedWriteConn wraps a net.Conn and gives control over how
// Writes get batched out.
type bufferedWriteConn struct {
net.Conn
w *bufio.Writer
manualFlush bool
}
func (c *bufferedWriteConn) Write(bs []byte) (int, error) {
n, err := c.w.Write(bs)
if err == nil && !c.manualFlush {
err = c.w.Flush()
}
return n, err
}
// TestFastPath exercises the Read codepath that can receive multiple
// Noise frames at once and decode each in turn without making another
// syscall.
func TestFastPath(t *testing.T) {
s1, s2 := tsnettest.NewConn("noise", 128000)
b := &bufferedWriteConn{s1, bufio.NewWriterSize(s1, 10000), false}
client, server := pairWithConns(t, b, s2)
b.manualFlush = true
sb := sinkReads(server)
const packets = 10
s := "test"
for i := 0; i < packets; i++ {
// Many separate writes, to force separate Noise frames that
// all get buffered up and then all sent as a single slice to
// the server.
if _, err := io.WriteString(client, s); err != nil {
t.Fatalf("client write1 failed: %v", err)
}
}
if err := b.w.Flush(); err != nil {
t.Fatalf("client flush failed: %v", err)
}
client.Close()
want := strings.Repeat(s, packets)
if got := sb.String(len(want)); got != want {
t.Fatalf("wrong content received: got %q, want %q", got, want)
}
if err := sb.Error(); err != io.EOF {
t.Fatalf("client close wasn't seen by server")
}
}
// Writes things larger than a single Noise frame, to check the
// chunking on the encoder and decoder.
func TestBigData(t *testing.T) {
client, server := pair(t)
serverReads := sinkReads(server)
clientReads := sinkReads(client)
const sz = 15 * 1024 // 15KiB
clientStr := strings.Repeat("abcde", sz/5)
serverStr := strings.Repeat("fghij", sz/5*2)
if _, err := io.WriteString(client, clientStr); err != nil {
t.Fatalf("writing client>server: %v", err)
}
if _, err := io.WriteString(server, serverStr); err != nil {
t.Fatalf("writing server>client: %v", err)
}
if serverGot := serverReads.String(sz); serverGot != clientStr {
t.Error("server didn't receive what client sent")
}
if clientGot := clientReads.String(2 * sz); clientGot != serverStr {
t.Error("client didn't receive what server sent")
}
getNonce := func(n [chp.NonceSize]byte) uint64 {
if binary.BigEndian.Uint32(n[:4]) != 0 {
panic("unexpected nonce")
}
return binary.BigEndian.Uint64(n[4:])
}
// Reach into the Conns and verify the cipher nonces advanced as
// expected.
if getNonce(client.tx.nonce) != getNonce(server.rx.nonce) {
t.Error("desynchronized client tx nonce")
}
if getNonce(server.tx.nonce) != getNonce(client.rx.nonce) {
t.Error("desynchronized server tx nonce")
}
if n := getNonce(client.tx.nonce); n != 4 {
t.Errorf("wrong client tx nonce, got %d want 4", n)
}
if n := getNonce(server.tx.nonce); n != 8 {
t.Errorf("wrong client tx nonce, got %d want 8", n)
}
}
// readerConn wraps a net.Conn and routes its Reads through a separate
// io.Reader.
type readerConn struct {
net.Conn
r io.Reader
}
func (c readerConn) Read(bs []byte) (int, error) { return c.r.Read(bs) }
// Check that the receiver can handle not being able to read an entire
// frame in a single syscall.
func TestDataTrickle(t *testing.T) {
s1, s2 := tsnettest.NewConn("noise", 128000)
client, server := pairWithConns(t, s1, readerConn{s2, iotest.OneByteReader(s2)})
serverReads := sinkReads(server)
const sz = 10000
clientStr := strings.Repeat("abcde", sz/5)
if _, err := io.WriteString(client, clientStr); err != nil {
t.Fatalf("writing client>server: %v", err)
}
serverGot := serverReads.String(sz)
if serverGot != clientStr {
t.Error("server didn't receive what client sent")
}
}
func TestConnStd(t *testing.T) {
// You can run this test manually, and noise.Conn should pass all
// of them except for TestConn/PastTimeout,
// TestConn/FutureTimeout, TestConn/ConcurrentMethods, because
// those tests assume that write errors are recoverable, and
// they're not on our Conn due to cipher security.
t.Skip("not all tests can pass on this Conn, see https://github.com/golang/go/issues/46977")
nettest.TestConn(t, func() (c1 net.Conn, c2 net.Conn, stop func(), err error) {
s1, s2 := tsnettest.NewConn("noise", 4096)
controlKey := key.NewMachine()
machineKey := key.NewMachine()
serverErr := make(chan error, 1)
go func() {
var err error
c2, err = Server(context.Background(), s2, controlKey)
serverErr <- err
}()
c1, err = Client(context.Background(), s1, machineKey, controlKey.Public())
if err != nil {
s1.Close()
s2.Close()
return nil, nil, nil, fmt.Errorf("connecting client: %w", err)
}
if err := <-serverErr; err != nil {
c1.Close()
s1.Close()
s2.Close()
return nil, nil, nil, fmt.Errorf("connecting server: %w", err)
}
return c1, c2, func() {
c1.Close()
c2.Close()
}, nil
})
}
// mkConns creates synthetic Noise Conns wrapping the given net.Conns.
// This function is for testing just the Conn transport logic without
// having to muck about with Noise handshakes.
func mkConns(s1, s2 net.Conn) (*Conn, *Conn) {
var k1, k2 [chp.KeySize]byte
if _, err := rand.Read(k1[:]); err != nil {
panic(err)
}
if _, err := rand.Read(k2[:]); err != nil {
panic(err)
}
ret1 := &Conn{
conn: s1,
tx: txState{cipher: newCHP(k1)},
rx: rxState{cipher: newCHP(k2)},
}
ret2 := &Conn{
conn: s2,
tx: txState{cipher: newCHP(k2)},
rx: rxState{cipher: newCHP(k1)},
}
return ret1, ret2
}
type readSink struct {
r io.Reader
cond *sync.Cond
sync.Mutex
bs bytes.Buffer
err error
}
func sinkReads(r io.Reader) *readSink {
ret := &readSink{
r: r,
}
ret.cond = sync.NewCond(&ret.Mutex)
go func() {
var buf [4096]byte
for {
n, err := r.Read(buf[:])
ret.Lock()
ret.bs.Write(buf[:n])
if err != nil {
ret.err = err
}
ret.cond.Broadcast()
ret.Unlock()
if err != nil {
return
}
}
}()
return ret
}
func (s *readSink) String(total int) string {
s.Lock()
defer s.Unlock()
for s.bs.Len() < total && s.err == nil {
s.cond.Wait()
}
if s.err != nil {
total = s.bs.Len()
}
return string(s.bs.Bytes()[:total])
}
func (s *readSink) Error() error {
s.Lock()
defer s.Unlock()
for s.err == nil {
s.cond.Wait()
}
return s.err
}
func (s *readSink) Total() int {
s.Lock()
defer s.Unlock()
return s.bs.Len()
}
func pairWithConns(t *testing.T, clientConn, serverConn net.Conn) (*Conn, *Conn) {
var (
controlKey = key.NewMachine()
machineKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)
go func() {
var err error
server, err = Server(context.Background(), serverConn, controlKey)
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, machineKey, controlKey.Public())
if err != nil {
t.Fatalf("client connection failed: %v", err)
}
if err := <-serverErr; err != nil {
t.Fatalf("server connection failed: %v", err)
}
return client, server
}
func pair(t *testing.T) (*Conn, *Conn) {
s1, s2 := tsnettest.NewConn("noise", 128000)
return pairWithConns(t, s1, s2)
}

443
control/noise/handshake.go Normal file
View File

@@ -0,0 +1,443 @@
// 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 noise
import (
"context"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"net"
"strconv"
"time"
"go4.org/mem"
"golang.org/x/crypto/blake2s"
chp "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"tailscale.com/types/key"
)
const (
// protocolName is the name of the specific instantiation of Noise
// that the control protocol uses. This string's value is fixed by
// the Noise spec, and shouldn't be changed unless we're updating
// the control protocol to use a different Noise instance.
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
// protocolVersion is the version of the control protocol that
// Client will use when initiating a handshake.
protocolVersion uint16 = 1
// protocolVersionPrefix is the name portion of the protocol
// name+version string that gets mixed into the handshake as a
// prologue.
//
// This mixing verifies that both clients agree that they're
// executing the control protocol at a specific version that
// matches the advertised version in the cleartext packet header.
protocolVersionPrefix = "Tailscale Control Protocol v"
invalidNonce = ^uint64(0)
)
func protocolVersionPrologue(version uint16) []byte {
ret := make([]byte, 0, len(protocolVersionPrefix)+5) // 5 bytes is enough to encode all possible version numbers.
ret = append(ret, protocolVersionPrefix...)
return strconv.AppendUint(ret, uint64(version), 10)
}
// Client initiates a control client handshake, returning the resulting
// control connection.
//
// The context deadline, if any, covers the entire handshaking
// process. Any preexisting Conn deadline is removed.
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer func() {
conn.SetDeadline(time.Time{})
}()
}
var s symmetricState
s.Initialize()
// prologue
s.MixHash(protocolVersionPrologue(protocolVersion))
// <- s
// ...
s.MixHash(controlKey.UntypedBytes())
// -> e, es, s, ss
init := mkInitiationMessage()
machineEphemeral := key.NewMachine()
machineEphemeralPub := machineEphemeral.Public()
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(machineEphemeral, controlKey)
if err != nil {
return nil, fmt.Errorf("computing es: %w", err)
}
machineKeyPub := machineKey.Public()
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub.UntypedBytes())
cipher, err = s.MixDH(machineKey, controlKey)
if err != nil {
return nil, fmt.Errorf("computing ss: %w", err)
}
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
if _, err := conn.Write(init[:]); err != nil {
return nil, fmt.Errorf("writing initiation: %w", err)
}
// Read in the payload and look for errors/protocol violations from the server.
var resp responseMessage
if _, err := io.ReadFull(conn, resp.Header()); err != nil {
return nil, fmt.Errorf("reading response header: %w", err)
}
if resp.Type() != msgTypeResponse {
if resp.Type() != msgTypeError {
return nil, fmt.Errorf("unexpected response message type %d", resp.Type())
}
msg := make([]byte, resp.Length())
if _, err := io.ReadFull(conn, msg); err != nil {
return nil, err
}
return nil, fmt.Errorf("server error: %q", msg)
}
if resp.Length() != len(resp.Payload()) {
return nil, fmt.Errorf("wrong length %d received for handshake response", resp.Length())
}
if _, err := io.ReadFull(conn, resp.Payload()); err != nil {
return nil, err
}
// <- e, ee, se
controlEphemeralPub := key.MachinePublicFromRaw32(mem.B(resp.EphemeralPub()))
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err = s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
cipher, err = s.MixDH(machineKey, controlEphemeralPub)
if err != nil {
return nil, fmt.Errorf("computing se: %w", err)
}
if err := s.DecryptAndHash(cipher, nil, resp.Tag()); err != nil {
return nil, fmt.Errorf("decrypting payload: %w", err)
}
c1, c2, err := s.Split()
if err != nil {
return nil, fmt.Errorf("finalizing handshake: %w", err)
}
c := &Conn{
conn: conn,
version: protocolVersion,
peer: controlKey,
handshakeHash: s.h,
tx: txState{
cipher: c1,
},
rx: rxState{
cipher: c2,
},
}
return c, nil
}
// Server initiates a control server handshake, returning the resulting
// control connection.
//
// The context deadline, if any, covers the entire handshaking
// process.
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer func() {
conn.SetDeadline(time.Time{})
}()
}
// Deliberately does not support formatting, so that we don't echo
// attacker-controlled input back to them.
sendErr := func(msg string) error {
if len(msg) >= 1<<16 {
msg = msg[:1<<16]
}
var hdr [headerLen]byte
hdr[0] = msgTypeError
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg)))
if _, err := conn.Write(hdr[:]); err != nil {
return fmt.Errorf("sending %q error to client: %w", msg, err)
}
if _, err := io.WriteString(conn, msg); err != nil {
return fmt.Errorf("sending %q error to client: %w", msg, err)
}
return fmt.Errorf("refused client handshake: %q", msg)
}
var s symmetricState
s.Initialize()
var init initiationMessage
if _, err := io.ReadFull(conn, init.Header()); err != nil {
return nil, err
}
if init.Version() != protocolVersion {
return nil, sendErr("unsupported protocol version")
}
if init.Type() != msgTypeInitiation {
return nil, sendErr("unexpected handshake message type")
}
if init.Length() != len(init.Payload()) {
return nil, sendErr("wrong handshake initiation length")
}
if _, err := io.ReadFull(conn, init.Payload()); err != nil {
return nil, err
}
// prologue. Can only do this once we at least think the client is
// handshaking using a supported version.
s.MixHash(protocolVersionPrologue(protocolVersion))
// <- s
// ...
controlKeyPub := controlKey.Public()
s.MixHash(controlKeyPub.UntypedBytes())
// -> e, es, s, ss
machineEphemeralPub := key.MachinePublicFromRaw32(mem.B(init.EphemeralPub()))
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(controlKey, machineEphemeralPub)
if err != nil {
return nil, fmt.Errorf("computing es: %w", err)
}
var machineKeyBytes [32]byte
if err := s.DecryptAndHash(cipher, machineKeyBytes[:], init.MachinePub()); err != nil {
return nil, fmt.Errorf("decrypting machine key: %w", err)
}
machineKey := key.MachinePublicFromRaw32(mem.B(machineKeyBytes[:]))
cipher, err = s.MixDH(controlKey, machineKey)
if err != nil {
return nil, fmt.Errorf("computing ss: %w", err)
}
if err := s.DecryptAndHash(cipher, nil, init.Tag()); err != nil {
return nil, fmt.Errorf("decrypting initiation tag: %w", err)
}
// <- e, ee, se
resp := mkResponseMessage()
controlEphemeral := key.NewMachine()
controlEphemeralPub := controlEphemeral.Public()
copy(resp.EphemeralPub(), controlEphemeralPub.UntypedBytes())
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err := s.MixDH(controlEphemeral, machineEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
cipher, err = s.MixDH(controlEphemeral, machineKey)
if err != nil {
return nil, fmt.Errorf("computing se: %w", err)
}
s.EncryptAndHash(cipher, resp.Tag(), nil) // empty message payload
c1, c2, err := s.Split()
if err != nil {
return nil, fmt.Errorf("finalizing handshake: %w", err)
}
if _, err := conn.Write(resp[:]); err != nil {
return nil, err
}
c := &Conn{
conn: conn,
version: protocolVersion,
peer: machineKey,
handshakeHash: s.h,
tx: txState{
cipher: c2,
},
rx: rxState{
cipher: c1,
},
}
return c, nil
}
// symmetricState contains the state of an in-flight handshake.
type symmetricState struct {
finished bool
h [blake2s.Size]byte // hash of currently-processed handshake state
ck [blake2s.Size]byte // chaining key used to construct session keys at the end of the handshake
}
func (s *symmetricState) checkFinished() {
if s.finished {
panic("attempted to use symmetricState after Split was called")
}
}
// Initialize sets s to the initial handshake state, prior to
// processing any handshake messages.
func (s *symmetricState) Initialize() {
s.checkFinished()
s.h = blake2s.Sum256([]byte(protocolName))
s.ck = s.h
}
// MixHash updates s.h to be BLAKE2s(s.h || data), where || is
// concatenation.
func (s *symmetricState) MixHash(data []byte) {
s.checkFinished()
h := newBLAKE2s()
h.Write(s.h[:])
h.Write(data)
h.Sum(s.h[:0])
}
// MixDH updates s.ck with the result of X25519(priv, pub) and returns
// a singleUseCHP that can be used to encrypt or decrypt handshake
// data.
//
// MixDH corresponds to MixKey(X25519(...))) in the spec. Implementing
// it as a single function allows for strongly-typed arguments that
// reduce the risk of error in the caller (e.g. invoking X25519 with
// two private keys, or two public keys), and thus producing the wrong
// calculation.
func (s *symmetricState) MixDH(priv key.MachinePrivate, pub key.MachinePublic) (*singleUseCHP, error) {
s.checkFinished()
keyData, err := curve25519.X25519(priv.UntypedBytes(), pub.UntypedBytes())
if err != nil {
return nil, fmt.Errorf("computing X25519: %w", err)
}
r := hkdf.New(newBLAKE2s, keyData, s.ck[:], nil)
if _, err := io.ReadFull(r, s.ck[:]); err != nil {
return nil, fmt.Errorf("extracting ck: %w", err)
}
var k [chp.KeySize]byte
if _, err := io.ReadFull(r, k[:]); err != nil {
return nil, fmt.Errorf("extracting k: %w", err)
}
return newSingleUseCHP(k), nil
}
// EncryptAndHash encrypts plaintext into ciphertext (which must be
// the correct size to hold the encrypted plaintext) using cipher,
// mixes the ciphertext into s.h, and returns the ciphertext.
func (s *symmetricState) EncryptAndHash(cipher *singleUseCHP, ciphertext, plaintext []byte) {
s.checkFinished()
if len(ciphertext) != len(plaintext)+chp.Overhead {
panic("ciphertext is wrong size for given plaintext")
}
ret := cipher.Seal(ciphertext[:0], plaintext, s.h[:])
s.MixHash(ret)
}
// DecryptAndHash decrypts the given ciphertext into plaintext (which
// must be the correct size to hold the decrypted ciphertext) using
// cipher. If decryption is successful, it mixes the ciphertext into
// s.h.
func (s *symmetricState) DecryptAndHash(cipher *singleUseCHP, plaintext, ciphertext []byte) error {
s.checkFinished()
if len(ciphertext) != len(plaintext)+chp.Overhead {
return errors.New("plaintext is wrong size for given ciphertext")
}
if _, err := cipher.Open(plaintext[:0], ciphertext, s.h[:]); err != nil {
return err
}
s.MixHash(ciphertext)
return nil
}
// Split returns two ChaCha20Poly1305 ciphers with keys derived from
// the current handshake state. Methods on s cannot be used again
// after calling Split.
func (s *symmetricState) Split() (c1, c2 cipher.AEAD, err error) {
s.finished = true
var k1, k2 [chp.KeySize]byte
r := hkdf.New(newBLAKE2s, nil, s.ck[:], nil)
if _, err := io.ReadFull(r, k1[:]); err != nil {
return nil, nil, fmt.Errorf("extracting k1: %w", err)
}
if _, err := io.ReadFull(r, k2[:]); err != nil {
return nil, nil, fmt.Errorf("extracting k2: %w", err)
}
c1, err = chp.New(k1[:])
if err != nil {
return nil, nil, fmt.Errorf("constructing AEAD c1: %w", err)
}
c2, err = chp.New(k2[:])
if err != nil {
return nil, nil, fmt.Errorf("constructing AEAD c2: %w", err)
}
return c1, c2, nil
}
// newBLAKE2s returns a hash.Hash implementing BLAKE2s, or panics on
// error.
func newBLAKE2s() hash.Hash {
h, err := blake2s.New256(nil)
if err != nil {
// Should never happen, errors only happen when using BLAKE2s
// in MAC mode with a key.
panic(err)
}
return h
}
// newCHP returns a cipher.AEAD implementing ChaCha20Poly1305, or
// panics on error.
func newCHP(key [chp.KeySize]byte) cipher.AEAD {
aead, err := chp.New(key[:])
if err != nil {
// Can only happen if we passed a key of the wrong length. The
// function signature prevents that.
panic(err)
}
return aead
}
// singleUseCHP is an instance of ChaCha20Poly1305 that can be used
// only once, either for encrypting or decrypting, but not both. The
// chosen operation is always executed with an all-zeros
// nonce. Subsequent calls to either Seal or Open panic.
type singleUseCHP struct {
c cipher.AEAD
}
func newSingleUseCHP(key [chp.KeySize]byte) *singleUseCHP {
return &singleUseCHP{newCHP(key)}
}
func (c *singleUseCHP) Seal(dst, plaintext, additionalData []byte) []byte {
if c.c == nil {
panic("Attempted reuse of singleUseAEAD")
}
cipher := c.c
c.c = nil
var nonce [chp.NonceSize]byte
return cipher.Seal(dst, nonce[:], plaintext, additionalData)
}
func (c *singleUseCHP) Open(dst, ciphertext, additionalData []byte) ([]byte, error) {
if c.c == nil {
panic("Attempted reuse of singleUseAEAD")
}
cipher := c.c
c.c = nil
var nonce [chp.NonceSize]byte
return cipher.Open(dst, nonce[:], ciphertext, additionalData)
}

View File

@@ -0,0 +1,299 @@
// 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 noise
import (
"bytes"
"context"
"io"
"strings"
"testing"
"time"
tsnettest "tailscale.com/net/nettest"
"tailscale.com/types/key"
)
func TestHandshake(t *testing.T) {
var (
clientConn, serverConn = tsnettest.NewConn("noise", 128000)
serverKey = key.NewMachine()
clientKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)
go func() {
var err error
server, err = Server(context.Background(), serverConn, serverKey)
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err != nil {
t.Fatalf("client connection failed: %v", err)
}
if err := <-serverErr; err != nil {
t.Fatalf("server connection failed: %v", err)
}
if client.HandshakeHash() != server.HandshakeHash() {
t.Fatal("client and server disagree on handshake hash")
}
if client.ProtocolVersion() != int(protocolVersion) {
t.Fatalf("client reporting wrong protocol version %d, want %d", client.ProtocolVersion(), protocolVersion)
}
if client.ProtocolVersion() != server.ProtocolVersion() {
t.Fatalf("peers disagree on protocol version, client=%d server=%d", client.ProtocolVersion(), server.ProtocolVersion())
}
if client.Peer() != serverKey.Public() {
t.Fatal("client peer key isn't serverKey")
}
if server.Peer() != clientKey.Public() {
t.Fatal("client peer key isn't serverKey")
}
}
// Check that handshaking repeatedly with the same long-term keys
// result in different handshake hashes and wire traffic.
func TestNoReuse(t *testing.T) {
var (
hashes = map[[32]byte]bool{}
clientHandshakes = map[[96]byte]bool{}
serverHandshakes = map[[48]byte]bool{}
packets = map[[32]byte]bool{}
)
for i := 0; i < 10; i++ {
var (
clientRaw, serverRaw = tsnettest.NewConn("noise", 128000)
clientBuf, serverBuf bytes.Buffer
clientConn = &readerConn{clientRaw, io.TeeReader(clientRaw, &clientBuf)}
serverConn = &readerConn{serverRaw, io.TeeReader(serverRaw, &serverBuf)}
serverKey = key.NewMachine()
clientKey = key.NewMachine()
server *Conn
serverErr = make(chan error, 1)
)
go func() {
var err error
server, err = Server(context.Background(), serverConn, serverKey)
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err != nil {
t.Fatalf("client connection failed: %v", err)
}
if err := <-serverErr; err != nil {
t.Fatalf("server connection failed: %v", err)
}
var clientHS [96]byte
copy(clientHS[:], serverBuf.Bytes())
if clientHandshakes[clientHS] {
t.Fatal("client handshake seen twice")
}
clientHandshakes[clientHS] = true
var serverHS [48]byte
copy(serverHS[:], clientBuf.Bytes())
if serverHandshakes[serverHS] {
t.Fatal("server handshake seen twice")
}
serverHandshakes[serverHS] = true
clientBuf.Reset()
serverBuf.Reset()
cb := sinkReads(client)
sb := sinkReads(server)
if hashes[client.HandshakeHash()] {
t.Fatalf("handshake hash %v seen twice", client.HandshakeHash())
}
hashes[client.HandshakeHash()] = true
// Sending 14 bytes turns into 32 bytes on the wire (+16 for
// the chacha20poly1305 overhead, +2 length header)
if _, err := io.WriteString(client, strings.Repeat("a", 14)); err != nil {
t.Fatalf("client>server write failed: %v", err)
}
if _, err := io.WriteString(server, strings.Repeat("b", 14)); err != nil {
t.Fatalf("server>client write failed: %v", err)
}
// Wait for the bytes to be read, so we know they've traveled end to end
cb.String(14)
sb.String(14)
var clientWire, serverWire [32]byte
copy(clientWire[:], clientBuf.Bytes())
copy(serverWire[:], serverBuf.Bytes())
if packets[clientWire] {
t.Fatalf("client wire traffic seen twice")
}
packets[clientWire] = true
if packets[serverWire] {
t.Fatalf("server wire traffic seen twice")
}
packets[serverWire] = true
server.Close()
client.Close()
}
}
// tamperReader wraps a reader and mutates the Nth byte.
type tamperReader struct {
r io.Reader
n int
total int
}
func (r *tamperReader) Read(bs []byte) (int, error) {
n, err := r.r.Read(bs)
if off := r.n - r.total; off >= 0 && off < n {
bs[off] += 1
}
r.total += n
return n, err
}
func TestTampering(t *testing.T) {
// Tamper with every byte of the client initiation message.
for i := 0; i < 101; i++ {
var (
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, i, 0}}
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
_, err := Server(context.Background(), serverConn, serverKey)
// If the server failed, we have to close the Conn to
// unblock the client.
if err != nil {
serverConn.Close()
}
serverErr <- err
}()
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err == nil {
t.Fatal("client connection succeeded despite tampering")
}
if err := <-serverErr; err == nil {
t.Fatalf("server connection succeeded despite tampering")
}
}
// Tamper with every byte of the server response message.
for i := 0; i < 51; i++ {
var (
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, i, 0}}
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
_, err := Server(context.Background(), serverConn, serverKey)
serverErr <- err
}()
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err == nil {
t.Fatal("client connection succeeded despite tampering")
}
// The server shouldn't fail, because the tampering took place
// in its response.
if err := <-serverErr; err != nil {
t.Fatalf("server connection failed despite no tampering: %v", err)
}
}
// Tamper with every byte of the first server>client transport message.
for i := 0; i < 30; i++ {
var (
clientRaw, serverConn = tsnettest.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, 51 + i, 0}}
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
server, err := Server(context.Background(), serverConn, serverKey)
serverErr <- err
_, err = io.WriteString(server, strings.Repeat("a", 14))
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err != nil {
t.Fatalf("client handshake failed: %v", err)
}
// The server shouldn't fail, because the tampering took place
// in its response.
if err := <-serverErr; err != nil {
t.Fatalf("server handshake failed: %v", err)
}
// The client needs a timeout if the tampering is hitting the length header.
if i == 1 || i == 2 {
client.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
}
var bs [100]byte
n, err := client.Read(bs[:])
if err == nil {
t.Fatal("read succeeded despite tampering")
}
if n != 0 {
t.Fatal("conn yielded some bytes despite tampering")
}
}
// Tamper with every byte of the first client>server transport message.
for i := 0; i < 30; i++ {
var (
clientConn, serverRaw = tsnettest.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, 101 + i, 0}}
serverKey = key.NewMachine()
clientKey = key.NewMachine()
serverErr = make(chan error, 1)
)
go func() {
server, err := Server(context.Background(), serverConn, serverKey)
serverErr <- err
var bs [100]byte
// The server needs a timeout if the tampering is hitting the length header.
if i == 1 || i == 2 {
server.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
}
n, err := server.Read(bs[:])
if n != 0 {
panic("server got bytes despite tampering")
} else {
serverErr <- err
}
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
if err != nil {
t.Fatalf("client handshake failed: %v", err)
}
if err := <-serverErr; err != nil {
t.Fatalf("server handshake failed: %v", err)
}
if _, err := io.WriteString(client, strings.Repeat("a", 14)); err != nil {
t.Fatalf("client>server write failed: %v", err)
}
if err := <-serverErr; err == nil {
t.Fatal("server successfully received bytes despite tampering")
}
}
}

View File

@@ -0,0 +1,257 @@
// 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 noise
import (
"context"
"encoding/binary"
"errors"
"io"
"net"
"testing"
tsnettest "tailscale.com/net/nettest"
"tailscale.com/types/key"
)
// Can a reference Noise IK client talk to our server?
func TestInteropClient(t *testing.T) {
var (
s1, s2 = tsnettest.NewConn("noise", 128000)
controlKey = key.NewMachine()
machineKey = key.NewMachine()
serverErr = make(chan error, 2)
serverBytes = make(chan []byte, 1)
c2s = "client>server"
s2c = "server>client"
)
go func() {
server, err := Server(context.Background(), s2, controlKey)
serverErr <- err
if err != nil {
return
}
var buf [1024]byte
_, err = io.ReadFull(server, buf[:len(c2s)])
serverBytes <- buf[:len(c2s)]
if err != nil {
serverErr <- err
return
}
_, err = server.Write([]byte(s2c))
serverErr <- err
}()
gotS2C, err := noiseExplorerClient(s1, controlKey.Public(), machineKey, []byte(c2s))
if err != nil {
t.Fatalf("failed client interop: %v", err)
}
if string(gotS2C) != s2c {
t.Fatalf("server sent unexpected data %q, want %q", string(gotS2C), s2c)
}
if err := <-serverErr; err != nil {
t.Fatalf("server handshake failed: %v", err)
}
if err := <-serverErr; err != nil {
t.Fatalf("server read/write failed: %v", err)
}
if got := string(<-serverBytes); got != c2s {
t.Fatalf("server received %q, want %q", got, c2s)
}
}
// Can our client talk to a reference Noise IK server?
func TestInteropServer(t *testing.T) {
var (
s1, s2 = tsnettest.NewConn("noise", 128000)
controlKey = key.NewMachine()
machineKey = key.NewMachine()
clientErr = make(chan error, 2)
clientBytes = make(chan []byte, 1)
c2s = "client>server"
s2c = "server>client"
)
go func() {
client, err := Client(context.Background(), s1, machineKey, controlKey.Public())
clientErr <- err
if err != nil {
return
}
_, err = client.Write([]byte(c2s))
if err != nil {
clientErr <- err
return
}
var buf [1024]byte
_, err = io.ReadFull(client, buf[:len(s2c)])
clientBytes <- buf[:len(s2c)]
clientErr <- err
}()
gotC2S, err := noiseExplorerServer(s2, controlKey, machineKey.Public(), []byte(s2c))
if err != nil {
t.Fatalf("failed server interop: %v", err)
}
if string(gotC2S) != c2s {
t.Fatalf("server sent unexpected data %q, want %q", string(gotC2S), c2s)
}
if err := <-clientErr; err != nil {
t.Fatalf("client handshake failed: %v", err)
}
if err := <-clientErr; err != nil {
t.Fatalf("client read/write failed: %v", err)
}
if got := string(<-clientBytes); got != s2c {
t.Fatalf("client received %q, want %q", got, s2c)
}
}
// noiseExplorerClient uses the Noise Explorer implementation of Noise
// IK to handshake as a Noise client on conn, transmit payload, and
// read+return a payload from the peer.
func noiseExplorerClient(conn net.Conn, controlKey key.MachinePublic, machineKey key.MachinePrivate, payload []byte) ([]byte, error) {
var mk keypair
copy(mk.private_key[:], machineKey.UntypedBytes())
copy(mk.public_key[:], machineKey.Public().UntypedBytes())
var peerKey [32]byte
copy(peerKey[:], controlKey.UntypedBytes())
session := InitSession(true, protocolVersionPrologue(protocolVersion), mk, peerKey)
_, msg1 := SendMessage(&session, nil)
var hdr [initiationHeaderLen]byte
binary.BigEndian.PutUint16(hdr[:2], protocolVersion)
hdr[2] = msgTypeInitiation
binary.BigEndian.PutUint16(hdr[3:5], 96)
if _, err := conn.Write(hdr[:]); err != nil {
return nil, err
}
if _, err := conn.Write(msg1.ne[:]); err != nil {
return nil, err
}
if _, err := conn.Write(msg1.ns); err != nil {
return nil, err
}
if _, err := conn.Write(msg1.ciphertext); err != nil {
return nil, err
}
var buf [1024]byte
if _, err := io.ReadFull(conn, buf[:51]); err != nil {
return nil, err
}
// ignore the header for this test, we're only checking the noise
// implementation.
msg2 := messagebuffer{
ciphertext: buf[35:51],
}
copy(msg2.ne[:], buf[3:35])
_, p, valid := RecvMessage(&session, &msg2)
if !valid {
return nil, errors.New("handshake failed")
}
if len(p) != 0 {
return nil, errors.New("non-empty payload")
}
_, msg3 := SendMessage(&session, payload)
hdr[0] = msgTypeRecord
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg3.ciphertext)))
if _, err := conn.Write(hdr[:3]); err != nil {
return nil, err
}
if _, err := conn.Write(msg3.ciphertext); err != nil {
return nil, err
}
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
return nil, err
}
// Ignore all of the header except the payload length
plen := int(binary.BigEndian.Uint16(buf[1:3]))
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
return nil, err
}
msg4 := messagebuffer{
ciphertext: buf[:plen],
}
_, p, valid = RecvMessage(&session, &msg4)
if !valid {
return nil, errors.New("transport message decryption failed")
}
return p, nil
}
func noiseExplorerServer(conn net.Conn, controlKey key.MachinePrivate, wantMachineKey key.MachinePublic, payload []byte) ([]byte, error) {
var mk keypair
copy(mk.private_key[:], controlKey.UntypedBytes())
copy(mk.public_key[:], controlKey.Public().UntypedBytes())
session := InitSession(false, protocolVersionPrologue(protocolVersion), mk, [32]byte{})
var buf [1024]byte
if _, err := io.ReadFull(conn, buf[:101]); err != nil {
return nil, err
}
// Ignore the header, we're just checking the noise implementation.
msg1 := messagebuffer{
ns: buf[37:85],
ciphertext: buf[85:101],
}
copy(msg1.ne[:], buf[5:37])
_, p, valid := RecvMessage(&session, &msg1)
if !valid {
return nil, errors.New("handshake failed")
}
if len(p) != 0 {
return nil, errors.New("non-empty payload")
}
_, msg2 := SendMessage(&session, nil)
var hdr [headerLen]byte
hdr[0] = msgTypeResponse
binary.BigEndian.PutUint16(hdr[1:3], 48)
if _, err := conn.Write(hdr[:]); err != nil {
return nil, err
}
if _, err := conn.Write(msg2.ne[:]); err != nil {
return nil, err
}
if _, err := conn.Write(msg2.ciphertext[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(conn, buf[:3]); err != nil {
return nil, err
}
plen := int(binary.BigEndian.Uint16(buf[1:3]))
if _, err := io.ReadFull(conn, buf[:plen]); err != nil {
return nil, err
}
msg3 := messagebuffer{
ciphertext: buf[:plen],
}
_, p, valid = RecvMessage(&session, &msg3)
if !valid {
return nil, errors.New("transport message decryption failed")
}
_, msg4 := SendMessage(&session, payload)
hdr[0] = msgTypeRecord
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg4.ciphertext)))
if _, err := conn.Write(hdr[:]); err != nil {
return nil, err
}
if _, err := conn.Write(msg4.ciphertext); err != nil {
return nil, err
}
return p, nil
}

88
control/noise/messages.go Normal file
View File

@@ -0,0 +1,88 @@
// 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 noise
import "encoding/binary"
const (
// msgTypeInitiation frames carry a Noise IK handshake initiation message.
msgTypeInitiation = 1
// msgTypeResponse frames carry a Noise IK handshake response message.
msgTypeResponse = 2
// msgTypeError frames carry an unauthenticated human-readable
// error message.
//
// Errors reported in this message type must be treated as public
// hints only. They are not encrypted or authenticated, and so can
// be seen and tampered with on the wire.
msgTypeError = 3
// msgTypeRecord frames carry session data bytes.
msgTypeRecord = 4
// headerLen is the size of the header on all messages except msgTypeInitiation.
headerLen = 3
// initiationHeaderLen is the size of the header on all msgTypeInitiation messages.
initiationHeaderLen = 5
)
// initiationMessage is the protocol message sent from a client
// machine to a control server.
//
// 2b: protocol version
// 1b: message type (0x01)
// 2b: payload length (96)
// 5b: header (see headerLen for fields)
// 32b: client ephemeral public key (cleartext)
// 48b: client machine public key (encrypted)
// 16b: message tag (authenticates the whole message)
type initiationMessage [101]byte
func mkInitiationMessage() initiationMessage {
var ret initiationMessage
binary.BigEndian.PutUint16(ret[:2], uint16(protocolVersion))
ret[2] = msgTypeInitiation
binary.BigEndian.PutUint16(ret[3:5], uint16(len(ret.Payload())))
return ret
}
func (m *initiationMessage) Header() []byte { return m[:initiationHeaderLen] }
func (m *initiationMessage) Payload() []byte { return m[initiationHeaderLen:] }
func (m *initiationMessage) Version() uint16 { return binary.BigEndian.Uint16(m[:2]) }
func (m *initiationMessage) Type() byte { return m[2] }
func (m *initiationMessage) Length() int { return int(binary.BigEndian.Uint16(m[3:5])) }
func (m *initiationMessage) EphemeralPub() []byte {
return m[initiationHeaderLen : initiationHeaderLen+32]
}
func (m *initiationMessage) MachinePub() []byte {
return m[initiationHeaderLen+32 : initiationHeaderLen+32+48]
}
func (m *initiationMessage) Tag() []byte { return m[initiationHeaderLen+32+48:] }
// responseMessage is the protocol message sent from a control server
// to a client machine.
//
// 1b: message type (0x02)
// 2b: payload length (48)
// 32b: control ephemeral public key (cleartext)
// 16b: message tag (authenticates the whole message)
type responseMessage [51]byte
func mkResponseMessage() responseMessage {
var ret responseMessage
ret[0] = msgTypeResponse
binary.BigEndian.PutUint16(ret[1:], uint16(len(ret.Payload())))
return ret
}
func (m *responseMessage) Header() []byte { return m[:headerLen] }
func (m *responseMessage) Payload() []byte { return m[headerLen:] }
func (m *responseMessage) Type() byte { return m[0] }
func (m *responseMessage) Length() int { return int(binary.BigEndian.Uint16(m[1:3])) }
func (m *responseMessage) EphemeralPub() []byte { return m[headerLen : headerLen+32] }
func (m *responseMessage) Tag() []byte { return m[headerLen+32:] }

View File

@@ -0,0 +1,475 @@
// This file contains the implementation of Noise IK from
// https://noiseexplorer.com/ . Unlike the rest of this repository,
// this file is licensed under the terms of the GNU GPL v3. See
// https://source.symbolic.software/noiseexplorer/noiseexplorer for
// more information.
//
// This file is used here to verify that Tailscale's implementation of
// Noise IK is interoperable with another implementation.
//lint:file-ignore SA4006 not our code.
/*
IK:
<- s
...
-> e, es, s, ss
<- e, ee, se
->
<-
*/
// Implementation Version: 1.0.2
/* ---------------------------------------------------------------- *
* PARAMETERS *
* ---------------------------------------------------------------- */
package noise
import (
"crypto/rand"
"crypto/subtle"
"encoding/binary"
"hash"
"io"
"math"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
/* ---------------------------------------------------------------- *
* TYPES *
* ---------------------------------------------------------------- */
type keypair struct {
public_key [32]byte
private_key [32]byte
}
type messagebuffer struct {
ne [32]byte
ns []byte
ciphertext []byte
}
type cipherstate struct {
k [32]byte
n uint32
}
type symmetricstate struct {
cs cipherstate
ck [32]byte
h [32]byte
}
type handshakestate struct {
ss symmetricstate
s keypair
e keypair
rs [32]byte
re [32]byte
psk [32]byte
}
type noisesession struct {
hs handshakestate
h [32]byte
cs1 cipherstate
cs2 cipherstate
mc uint64
i bool
}
/* ---------------------------------------------------------------- *
* CONSTANTS *
* ---------------------------------------------------------------- */
var emptyKey = [32]byte{
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
}
var minNonce = uint32(0)
/* ---------------------------------------------------------------- *
* UTILITY FUNCTIONS *
* ---------------------------------------------------------------- */
func getPublicKey(kp *keypair) [32]byte {
return kp.public_key
}
func isEmptyKey(k [32]byte) bool {
return subtle.ConstantTimeCompare(k[:], emptyKey[:]) == 1
}
func validatePublicKey(k []byte) bool {
forbiddenCurveValues := [12][]byte{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{224, 235, 122, 124, 59, 65, 184, 174, 22, 86, 227, 250, 241, 159, 196, 106, 218, 9, 141, 235, 156, 50, 177, 253, 134, 98, 5, 22, 95, 73, 184, 0},
{95, 156, 149, 188, 163, 80, 140, 36, 177, 208, 177, 85, 156, 131, 239, 91, 4, 68, 92, 196, 88, 28, 142, 134, 216, 34, 78, 221, 208, 159, 17, 87},
{236, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
{237, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
{238, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127},
{205, 235, 122, 124, 59, 65, 184, 174, 22, 86, 227, 250, 241, 159, 196, 106, 218, 9, 141, 235, 156, 50, 177, 253, 134, 98, 5, 22, 95, 73, 184, 128},
{76, 156, 149, 188, 163, 80, 140, 36, 177, 208, 177, 85, 156, 131, 239, 91, 4, 68, 92, 196, 88, 28, 142, 134, 216, 34, 78, 221, 208, 159, 17, 215},
{217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
{218, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
{219, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 25},
}
for _, testValue := range forbiddenCurveValues {
if subtle.ConstantTimeCompare(k[:], testValue[:]) == 1 {
panic("Invalid public key")
}
}
return true
}
/* ---------------------------------------------------------------- *
* PRIMITIVES *
* ---------------------------------------------------------------- */
func incrementNonce(n uint32) uint32 {
return n + 1
}
func dh(private_key [32]byte, public_key [32]byte) [32]byte {
var ss [32]byte
curve25519.ScalarMult(&ss, &private_key, &public_key)
return ss
}
func generateKeypair() keypair {
var public_key [32]byte
var private_key [32]byte
_, _ = rand.Read(private_key[:])
curve25519.ScalarBaseMult(&public_key, &private_key)
if validatePublicKey(public_key[:]) {
return keypair{public_key, private_key}
}
return generateKeypair()
}
func generatePublicKey(private_key [32]byte) [32]byte {
var public_key [32]byte
curve25519.ScalarBaseMult(&public_key, &private_key)
return public_key
}
func encrypt(k [32]byte, n uint32, ad []byte, plaintext []byte) []byte {
var nonce [12]byte
var ciphertext []byte
enc, _ := chacha20poly1305.New(k[:])
binary.LittleEndian.PutUint32(nonce[4:], n)
ciphertext = enc.Seal(nil, nonce[:], plaintext, ad)
return ciphertext
}
func decrypt(k [32]byte, n uint32, ad []byte, ciphertext []byte) (bool, []byte, []byte) {
var nonce [12]byte
var plaintext []byte
enc, err := chacha20poly1305.New(k[:])
binary.LittleEndian.PutUint32(nonce[4:], n)
plaintext, err = enc.Open(nil, nonce[:], ciphertext, ad)
return (err == nil), ad, plaintext
}
func getHash(a []byte, b []byte) [32]byte {
return blake2s.Sum256(append(a, b...))
}
func hashProtocolName(protocolName []byte) [32]byte {
var h [32]byte
if len(protocolName) <= 32 {
copy(h[:], protocolName)
} else {
h = getHash(protocolName, []byte{})
}
return h
}
func blake2HkdfInterface() hash.Hash {
h, _ := blake2s.New256([]byte{})
return h
}
func getHkdf(ck [32]byte, ikm []byte) ([32]byte, [32]byte, [32]byte) {
var k1 [32]byte
var k2 [32]byte
var k3 [32]byte
output := hkdf.New(blake2HkdfInterface, ikm[:], ck[:], []byte{})
io.ReadFull(output, k1[:])
io.ReadFull(output, k2[:])
io.ReadFull(output, k3[:])
return k1, k2, k3
}
/* ---------------------------------------------------------------- *
* STATE MANAGEMENT *
* ---------------------------------------------------------------- */
/* CipherState */
func initializeKey(k [32]byte) cipherstate {
return cipherstate{k, minNonce}
}
func hasKey(cs *cipherstate) bool {
return !isEmptyKey(cs.k)
}
func setNonce(cs *cipherstate, newNonce uint32) *cipherstate {
cs.n = newNonce
return cs
}
func encryptWithAd(cs *cipherstate, ad []byte, plaintext []byte) (*cipherstate, []byte) {
e := encrypt(cs.k, cs.n, ad, plaintext)
cs = setNonce(cs, incrementNonce(cs.n))
return cs, e
}
func decryptWithAd(cs *cipherstate, ad []byte, ciphertext []byte) (*cipherstate, []byte, bool) {
valid, ad, plaintext := decrypt(cs.k, cs.n, ad, ciphertext)
cs = setNonce(cs, incrementNonce(cs.n))
return cs, plaintext, valid
}
func reKey(cs *cipherstate) *cipherstate {
e := encrypt(cs.k, math.MaxUint32, []byte{}, emptyKey[:])
copy(cs.k[:], e)
return cs
}
/* SymmetricState */
func initializeSymmetric(protocolName []byte) symmetricstate {
h := hashProtocolName(protocolName)
ck := h
cs := initializeKey(emptyKey)
return symmetricstate{cs, ck, h}
}
func mixKey(ss *symmetricstate, ikm [32]byte) *symmetricstate {
ck, tempK, _ := getHkdf(ss.ck, ikm[:])
ss.cs = initializeKey(tempK)
ss.ck = ck
return ss
}
func mixHash(ss *symmetricstate, data []byte) *symmetricstate {
ss.h = getHash(ss.h[:], data)
return ss
}
func mixKeyAndHash(ss *symmetricstate, ikm [32]byte) *symmetricstate {
var tempH [32]byte
var tempK [32]byte
ss.ck, tempH, tempK = getHkdf(ss.ck, ikm[:])
ss = mixHash(ss, tempH[:])
ss.cs = initializeKey(tempK)
return ss
}
func getHandshakeHash(ss *symmetricstate) [32]byte {
return ss.h
}
func encryptAndHash(ss *symmetricstate, plaintext []byte) (*symmetricstate, []byte) {
var ciphertext []byte
if hasKey(&ss.cs) {
_, ciphertext = encryptWithAd(&ss.cs, ss.h[:], plaintext)
} else {
ciphertext = plaintext
}
ss = mixHash(ss, ciphertext)
return ss, ciphertext
}
func decryptAndHash(ss *symmetricstate, ciphertext []byte) (*symmetricstate, []byte, bool) {
var plaintext []byte
var valid bool
if hasKey(&ss.cs) {
_, plaintext, valid = decryptWithAd(&ss.cs, ss.h[:], ciphertext)
} else {
plaintext, valid = ciphertext, true
}
ss = mixHash(ss, ciphertext)
return ss, plaintext, valid
}
func split(ss *symmetricstate) (cipherstate, cipherstate) {
tempK1, tempK2, _ := getHkdf(ss.ck, []byte{})
cs1 := initializeKey(tempK1)
cs2 := initializeKey(tempK2)
return cs1, cs2
}
/* HandshakeState */
func initializeInitiator(prologue []byte, s keypair, rs [32]byte, psk [32]byte) handshakestate {
var ss symmetricstate
var e keypair
var re [32]byte
name := []byte("Noise_IK_25519_ChaChaPoly_BLAKE2s")
ss = initializeSymmetric(name)
mixHash(&ss, prologue)
mixHash(&ss, rs[:])
return handshakestate{ss, s, e, rs, re, psk}
}
func initializeResponder(prologue []byte, s keypair, rs [32]byte, psk [32]byte) handshakestate {
var ss symmetricstate
var e keypair
var re [32]byte
name := []byte("Noise_IK_25519_ChaChaPoly_BLAKE2s")
ss = initializeSymmetric(name)
mixHash(&ss, prologue)
mixHash(&ss, s.public_key[:])
return handshakestate{ss, s, e, rs, re, psk}
}
func writeMessageA(hs *handshakestate, payload []byte) (*handshakestate, messagebuffer) {
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
hs.e = generateKeypair()
ne = hs.e.public_key
mixHash(&hs.ss, ne[:])
/* No PSK, so skipping mixKey */
mixKey(&hs.ss, dh(hs.e.private_key, hs.rs))
spk := make([]byte, len(hs.s.public_key))
copy(spk[:], hs.s.public_key[:])
_, ns = encryptAndHash(&hs.ss, spk)
mixKey(&hs.ss, dh(hs.s.private_key, hs.rs))
_, ciphertext = encryptAndHash(&hs.ss, payload)
messageBuffer := messagebuffer{ne, ns, ciphertext}
return hs, messageBuffer
}
func writeMessageB(hs *handshakestate, payload []byte) ([32]byte, messagebuffer, cipherstate, cipherstate) {
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
hs.e = generateKeypair()
ne = hs.e.public_key
mixHash(&hs.ss, ne[:])
/* No PSK, so skipping mixKey */
mixKey(&hs.ss, dh(hs.e.private_key, hs.re))
mixKey(&hs.ss, dh(hs.e.private_key, hs.rs))
_, ciphertext = encryptAndHash(&hs.ss, payload)
messageBuffer := messagebuffer{ne, ns, ciphertext}
cs1, cs2 := split(&hs.ss)
return hs.ss.h, messageBuffer, cs1, cs2
}
func writeMessageRegular(cs *cipherstate, payload []byte) (*cipherstate, messagebuffer) {
ne, ns, ciphertext := emptyKey, []byte{}, []byte{}
cs, ciphertext = encryptWithAd(cs, []byte{}, payload)
messageBuffer := messagebuffer{ne, ns, ciphertext}
return cs, messageBuffer
}
func readMessageA(hs *handshakestate, message *messagebuffer) (*handshakestate, []byte, bool) {
valid1 := true
if validatePublicKey(message.ne[:]) {
hs.re = message.ne
}
mixHash(&hs.ss, hs.re[:])
/* No PSK, so skipping mixKey */
mixKey(&hs.ss, dh(hs.s.private_key, hs.re))
_, ns, valid1 := decryptAndHash(&hs.ss, message.ns)
if valid1 && len(ns) == 32 && validatePublicKey(message.ns[:]) {
copy(hs.rs[:], ns)
}
mixKey(&hs.ss, dh(hs.s.private_key, hs.rs))
_, plaintext, valid2 := decryptAndHash(&hs.ss, message.ciphertext)
return hs, plaintext, (valid1 && valid2)
}
func readMessageB(hs *handshakestate, message *messagebuffer) ([32]byte, []byte, bool, cipherstate, cipherstate) {
valid1 := true
if validatePublicKey(message.ne[:]) {
hs.re = message.ne
}
mixHash(&hs.ss, hs.re[:])
/* No PSK, so skipping mixKey */
mixKey(&hs.ss, dh(hs.e.private_key, hs.re))
mixKey(&hs.ss, dh(hs.s.private_key, hs.re))
_, plaintext, valid2 := decryptAndHash(&hs.ss, message.ciphertext)
cs1, cs2 := split(&hs.ss)
return hs.ss.h, plaintext, (valid1 && valid2), cs1, cs2
}
func readMessageRegular(cs *cipherstate, message *messagebuffer) (*cipherstate, []byte, bool) {
/* No encrypted keys */
_, plaintext, valid2 := decryptWithAd(cs, []byte{}, message.ciphertext)
return cs, plaintext, valid2
}
/* ---------------------------------------------------------------- *
* PROCESSES *
* ---------------------------------------------------------------- */
func InitSession(initiator bool, prologue []byte, s keypair, rs [32]byte) noisesession {
var session noisesession
psk := emptyKey
if initiator {
session.hs = initializeInitiator(prologue, s, rs, psk)
} else {
session.hs = initializeResponder(prologue, s, rs, psk)
}
session.i = initiator
session.mc = 0
return session
}
func SendMessage(session *noisesession, message []byte) (*noisesession, messagebuffer) {
var messageBuffer messagebuffer
if session.mc == 0 {
_, messageBuffer = writeMessageA(&session.hs, message)
}
if session.mc == 1 {
session.h, messageBuffer, session.cs1, session.cs2 = writeMessageB(&session.hs, message)
session.hs = handshakestate{}
}
if session.mc > 1 {
if session.i {
_, messageBuffer = writeMessageRegular(&session.cs1, message)
} else {
_, messageBuffer = writeMessageRegular(&session.cs2, message)
}
}
session.mc = session.mc + 1
return session, messageBuffer
}
func RecvMessage(session *noisesession, message *messagebuffer) (*noisesession, []byte, bool) {
var plaintext []byte
var valid bool
if session.mc == 0 {
_, plaintext, valid = readMessageA(&session.hs, message)
}
if session.mc == 1 {
session.h, plaintext, valid, session.cs1, session.cs2 = readMessageB(&session.hs, message)
session.hs = handshakestate{}
}
if session.mc > 1 {
if session.i {
_, plaintext, valid = readMessageRegular(&session.cs2, message)
} else {
_, plaintext, valid = readMessageRegular(&session.cs1, message)
}
}
session.mc = session.mc + 1
return session, plaintext, valid
}
func main() {}

View File

@@ -210,12 +210,12 @@ func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
c.wmu.Lock()
defer c.wmu.Unlock()
if c.rate != nil {
pktLen := frameHeaderLen + dstKey.RawLen() + len(pkt)
pktLen := frameHeaderLen + key.NodePublicRawLen + len(pkt)
if !c.rate.AllowN(time.Now(), pktLen) {
return nil // drop
}
}
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(dstKey.RawLen()+len(pkt))); err != nil {
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(key.NodePublicRawLen+len(pkt))); err != nil {
return err
}
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {

View File

@@ -1017,7 +1017,7 @@ func (s *Server) verifyClient(clientKey key.NodePublic, info *clientInfo) error
}
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
buf := make([]byte, 0, len(magic)+s.publicKey.RawLen())
buf := make([]byte, 0, len(magic)+key.NodePublicRawLen)
buf = append(buf, magic...)
buf = s.publicKey.AppendTo(buf)
err := writeFrame(lw.bw(), frameServerKey, buf)
@@ -1469,7 +1469,7 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
withKey := !srcKey.IsZero()
pktLen := len(contents)
if withKey {
pktLen += srcKey.RawLen()
pktLen += key.NodePublicRawLen
}
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
return err

View File

@@ -429,7 +429,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
}
hostOrIP := host
dialer := netns.NewDialer()
dialer := netns.NewDialer(c.logf)
if c.DNSCache != nil {
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
@@ -519,7 +519,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
return netns.NewDialer().DialContext(ctx, proto, addr)
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
}
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6

View File

@@ -25,8 +25,9 @@ import (
"fmt"
"net"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// Magic is the 6 byte header of all discovery messages.
@@ -115,19 +116,19 @@ type Ping struct {
// It shouldn't be trusted by itself, but can be combined with
// netmap data to reduce the discokey:nodekey relation from 1:N to
// 1:1.
NodeKey tailcfg.NodeKey
NodeKey key.NodePublic
}
func (m *Ping) AppendMarshal(b []byte) []byte {
dataLen := 12
hasKey := !m.NodeKey.IsZero()
if hasKey {
dataLen += len(m.NodeKey)
dataLen += key.NodePublicRawLen
}
ret, d := appendMsgHeader(b, TypePing, v0, dataLen)
n := copy(d, m.TxID[:])
if hasKey {
copy(d[n:], m.NodeKey[:])
m.NodeKey.AppendTo(d[:n])
}
return ret
}
@@ -138,8 +139,10 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
}
m = new(Ping)
p = p[copy(m.TxID[:], p):]
if len(p) >= len(m.NodeKey) {
copy(m.NodeKey[:], p)
// Deliberately lax on longer-than-expected messages, for future
// compatibility.
if len(p) >= key.NodePublicRawLen {
m.NodeKey = key.NodePublicFromRaw32(mem.B(p[:key.NodePublicRawLen]))
}
return m, nil
}

View File

@@ -10,8 +10,9 @@ import (
"strings"
"testing"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func TestMarshalAndParse(t *testing.T) {
@@ -30,13 +31,8 @@ func TestMarshalAndParse(t *testing.T) {
{
name: "ping_with_nodekey_src",
m: &Ping{
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
NodeKey: tailcfg.NodeKey{
1: 1,
2: 2,
30: 30,
31: 31,
},
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
NodeKey: key.NodePublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 30: 30, 31: 31})),
},
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1e 1f",
},

View File

@@ -32,7 +32,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
```
### Sample Sidecar
Running as a sidecar allows you to directly expose a Kubernetes pod over Tailscale. This is particularly useful if you do not wish to expose a service on the public internet. This method allows bi-directional connectivty between the pod and other devices on the Tailnet. You can use [ACLs](https://tailscale.com/kb/1018/acls/) to control traffic flow.
Running as a sidecar allows you to directly expose a Kubernetes pod over Tailscale. This is particularly useful if you do not wish to expose a service on the public internet. This method allows bi-directional connectivity between the pod and other devices on the Tailnet. You can use [ACLs](https://tailscale.com/kb/1018/acls/) to control traffic flow.
1. Create and login to the sample nginx pod with a Tailscale sidecar
@@ -144,4 +144,4 @@ routes for the subnet-router are enabled.
# INTERNAL_IP="$(kubectl get po <POD_NAME> -o=jsonpath='{.status.podIP}')"
INTERNAL_PORT=8080
curl http://$INTERNAL_IP:$INTERNAL_PORT
```
```

261
go.mod
View File

@@ -4,209 +4,260 @@ go 1.17
require (
filippo.io/mkcert v1.4.3
github.com/akutz/memconn v0.1.0
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go v1.38.52
github.com/aws/aws-sdk-go-v2 v1.9.2
github.com/aws/aws-sdk-go-v2/config v1.8.3
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
github.com/aws/aws-sdk-go-v2 v1.11.2
github.com/aws/aws-sdk-go-v2/config v1.11.0
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
github.com/coreos/go-iptables v0.6.0
github.com/creack/pty v1.1.17
github.com/dave/jennifer v1.4.1
github.com/frankban/quicktest v1.13.1
github.com/frankban/quicktest v1.14.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-multierror/multierror v1.0.2
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
github.com/godbus/dbus/v5 v5.0.5
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.0.6
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.3.0
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/insomniacslk/dhcp v0.0.0-20211026125128-ad197bcd36fd
github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.13.6
github.com/mdlayher/netlink v1.4.1
github.com/mdlayher/netlink v1.4.2
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
github.com/miekg/dns v1.1.43
github.com/mitchellh/go-ps v1.0.0
github.com/pborman/getopt v1.1.0
github.com/peterbourgon/ff/v3 v3.1.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/pkg/sftp v1.13.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/vishvananda/netlink v1.1.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20211020060615-d418f374d309
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6
go4.org/mem v0.0.0-20210711025021-927187094b94
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20211020174200-9d6173849985
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
golang.org/x/tools v0.1.7
golang.zx2c4.com/wireguard v0.0.0-20211020205005-82e0b734e5d2
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.8
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
golang.zx2c4.com/wireguard/windows v0.4.10
honnef.co/go/tools v0.2.1
honnef.co/go/tools v0.2.2
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
inet.af/netstack v0.0.0-20211027215559-ec21145de76b
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
nhooyr.io/websocket v1.8.7
)
require (
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
4d63.com/gochecknoglobals v0.1.0 // indirect
github.com/Antonboom/errname v0.1.5 // indirect
github.com/Antonboom/nilnil v0.1.0 // indirect
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 // indirect
github.com/aws/smithy-go v1.8.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/ashanbrown/forbidigo v1.2.0 // indirect
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
github.com/blizzy78/varnamelen v0.5.0 // indirect
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
github.com/breml/bidichk v0.2.1 // indirect
github.com/butuzov/ireturn v0.1.1 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
github.com/daixiang0/gci v0.2.7 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
github.com/daixiang0/gci v0.2.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingajkin/go-header v0.3.1 // indirect
github.com/denis-tingajkin/go-header v0.4.2 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-critic/go-critic v0.5.2 // indirect
github.com/esimonov/ifshort v1.0.3 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/go-critic/go-critic v0.6.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.0.0 // indirect
github.com/go-git/go-git/v5 v5.2.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-toolsmith/astcast v1.0.0 // indirect
github.com/go-toolsmith/astcopy v1.0.0 // indirect
github.com/go-toolsmith/astequal v1.0.0 // indirect
github.com/go-toolsmith/astequal v1.0.1 // indirect
github.com/go-toolsmith/astfmt v1.0.0 // indirect
github.com/go-toolsmith/astp v1.0.0 // indirect
github.com/go-toolsmith/strparse v1.0.0 // indirect
github.com/go-toolsmith/typep v1.0.2 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.8.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
github.com/golangci/golangci-lint v1.33.0 // indirect
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc // indirect
github.com/golangci/golangci-lint v1.43.0 // indirect
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
github.com/golangci/misspell v0.3.5 // indirect
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
github.com/goreleaser/chglog v0.1.2 // indirect
github.com/goreleaser/fileglob v0.3.1 // indirect
github.com/gostaticanalysis/analysisutil v0.6.1 // indirect
github.com/gostaticanalysis/comment v1.4.1 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 // indirect
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect
github.com/jgautheron/goconst v1.5.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/kisielk/errcheck v1.6.0 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kunwardeep/paralleltest v1.0.2 // indirect
github.com/kulti/thelper v0.4.0 // indirect
github.com/kunwardeep/paralleltest v1.0.3 // indirect
github.com/kyoh86/exportloopref v0.1.8 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/ldez/gomoddirectives v0.2.2 // indirect
github.com/ldez/tagliatelle v0.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/maratori/testpackage v1.0.1 // indirect
github.com/matoous/godox v0.0.0-20200801072554-4fb83dc2941e // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mbilski/exhaustivestruct v1.1.0 // indirect
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
github.com/mgechev/revive v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moricho/tparallel v0.2.1 // indirect
github.com/nakabonne/nestif v0.3.0 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
github.com/nishanths/exhaustive v0.1.0 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/nishanths/exhaustive v0.7.11 // indirect
github.com/nishanths/predeclared v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f // indirect
github.com/quasilyte/go-ruleguard v0.2.1 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c // indirect
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/ryancurrah/gomodguard v1.1.0 // indirect
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.3.13 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/ryancurrah/gomodguard v1.2.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
github.com/securego/gosec/v2 v2.5.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/securego/gosec/v2 v2.9.3 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sivchari/tenv v1.4.7 // indirect
github.com/sonatard/noctx v0.0.1 // indirect
github.com/sourcegraph/go-diff v0.6.1 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.1 // indirect
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
github.com/spf13/viper v1.9.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect
github.com/tetafro/godot v1.3.2 // indirect
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 // indirect
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa // indirect
github.com/sylvia7788/contextcheck v1.0.4 // indirect
github.com/tdakkota/asciicheck v0.1.1 // indirect
github.com/tetafro/godot v1.4.11 // indirect
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.4 // indirect
github.com/uudashr/gocognit v1.0.1 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/uudashr/gocognit v1.0.5 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yeya24/promlinter v0.1.0 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
howett.net/plist v1.0.0 // indirect
mvdan.cc/gofumpt v0.2.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 // indirect
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 // indirect
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 // indirect
)

1130
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,8 @@ import (
"sync/atomic"
"time"
"github.com/go-multierror/multierror"
"tailscale.com/tailcfg"
"tailscale.com/util/multierr"
)
var (
@@ -58,6 +58,9 @@ const (
// SysDNS is the name of the net/dns subsystem.
SysDNS = Subsystem("dns")
// SysDNSOS is the name of the net/dns OSConfigurator subsystem.
SysDNSOS = Subsystem("dns-os")
// SysNetworkCategory is the name of the subsystem that sets
// the Windows network adapter's "category" (public, private, domain).
// If it's unhealthy, the Windows firewall rules won't match.
@@ -101,6 +104,12 @@ func SetDNSHealth(err error) { set(SysDNS, err) }
// DNSHealth returns the net/dns.Manager error state.
func DNSHealth() error { return get(SysDNS) }
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator
func SetDNSOSHealth(err error) { set(SysDNSOS, err) }
// DNSOSHealth returns the net/dns.OSConfigurator error state.
func DNSOSHealth() error { return get(SysDNSOS) }
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
// This only applies on Windows.
func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
@@ -268,7 +277,7 @@ func selfCheckLocked() {
// OverallError returns a summary of the health state.
//
// If there are multiple problems, the error will be of type
// multierror.MultipleErrors.
// multierr.Error.
func OverallError() error {
mu.Lock()
defer mu.Unlock()
@@ -337,7 +346,7 @@ func overallErrorLocked() error {
// Not super efficient (stringifying these in a sort), but probably max 2 or 3 items.
return errs[i].Error() < errs[j].Error()
})
return multierror.New(errs)
return multierr.New(errs...)
}
var (

View File

@@ -7,11 +7,14 @@
package hostinfo
import (
"bufio"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"time"
"go4.org/mem"
"tailscale.com/tailcfg"
@@ -84,6 +87,7 @@ const (
AWSFargate = EnvType("fg")
FlyDotIo = EnvType("fly")
Kubernetes = EnvType("k8s")
DockerDesktop = EnvType("dde")
)
var envType atomic.Value // of EnvType
@@ -141,6 +145,9 @@ func getEnvType() EnvType {
if inKubernetes() {
return Kubernetes
}
if inDockerDesktop() {
return DockerDesktop
}
return ""
}
@@ -224,3 +231,62 @@ func inKubernetes() bool {
}
return false
}
func inDockerDesktop() bool {
if os.Getenv("TS_HOST_ENV") == "dde" {
return true
}
return false
}
type etcAptSrcResult struct {
mod time.Time
disabled bool
}
var etcAptSrcCache atomic.Value // of etcAptSrcResult
// DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled
// the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade
// to a new release of the distro.
//
// See https://github.com/tailscale/tailscale/issues/3177
func DisabledEtcAptSource() bool {
if runtime.GOOS != "linux" {
return false
}
const path = "/etc/apt/sources.list.d/tailscale.list"
fi, err := os.Stat(path)
if err != nil || !fi.Mode().IsRegular() {
return false
}
mod := fi.ModTime()
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod == mod {
return c.disabled
}
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
v := etcAptSourceFileIsDisabled(f)
etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v})
return v
}
func etcAptSourceFileIsDisabled(r io.Reader) bool {
bs := bufio.NewScanner(r)
disabled := false // did we find the "disabled on upgrade" comment?
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if strings.Contains(line, "# disabled on upgrade") {
disabled = true
}
if line == "" || line[0] == '#' {
continue
}
// Well, it has some contents in it at least.
return false
}
return disabled
}

View File

@@ -6,6 +6,7 @@ package hostinfo
import (
"encoding/json"
"strings"
"testing"
)
@@ -27,3 +28,25 @@ func TestOSVersion(t *testing.T) {
}
t.Logf("Got: %#q", osVersion())
}
func TestEtcAptSourceFileIsDisabled(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{"empty", "", false},
{"normal", "deb foo\n", false},
{"normal-commented", "# deb foo\n", false},
{"normal-disabled-by-ubuntu", "# deb foo # disabled on upgrade to dingus\n", true},
{"normal-disabled-then-uncommented", "deb foo # disabled on upgrade to dingus\n", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := etcAptSourceFileIsDisabled(strings.NewReader(tt.in))
if got != tt.want {
t.Errorf("got %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -5,10 +5,11 @@
package hostinfo
import (
"os/exec"
"strings"
"fmt"
"sync/atomic"
"syscall"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func init() {
@@ -21,19 +22,37 @@ func osVersionWindows() string {
if s, ok := winVerCache.Load().(string); ok {
return s
}
cmd := exec.Command("cmd", "/c", "ver")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
s := strings.TrimSpace(string(out))
s = strings.TrimPrefix(s, "Microsoft Windows [")
s = strings.TrimSuffix(s, "]")
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
if sp := strings.Index(s, " "); sp != -1 {
s = s[sp+1:]
major, minor, build := windows.RtlGetNtVersionNumbers()
s := fmt.Sprintf("%d.%d.%d", major, minor, build)
// Windows 11 still uses 10 as its major number internally
if major == 10 {
if ubr, err := getUBR(); err == nil {
s += fmt.Sprintf(".%d", ubr)
}
}
if s != "" {
winVerCache.Store(s)
}
return s // "10.0.19041.388", ideally
}
// getUBR obtains a fourth version field, the "Update Build Revision",
// from the registry. This field is only available beginning with Windows 10.
func getUBR() (uint32, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE|registry.WOW64_64KEY)
if err != nil {
return 0, err
}
defer key.Close()
val, valType, err := key.GetIntegerValue("UBR")
if err != nil {
return 0, err
}
if valType != registry.DWORD {
return 0, registry.ErrUnexpectedType
}
return uint32(val), nil
}

View File

@@ -12,6 +12,7 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/structs"
)
@@ -48,7 +49,7 @@ type EngineStatus struct {
RBytes, WBytes int64
NumLive int
LiveDERPs int // number of active DERP connections
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
LivePeers map[key.NodePublic]ipnstate.PeerStatusLite
}
// Notify is a communication from a backend (e.g. tailscaled) to a frontend

View File

@@ -232,32 +232,11 @@ func TestDNSConfigForNetmap(t *testing.T) {
},
},
{
name: "android_does_need_fallbacks",
os: "android",
nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
FallbackResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4"},
},
Routes: map[string][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4:53"},
},
Routes: map[dnsname.FQDN][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4:53"}},
},
},
},
{
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
// Android had cases where it needed FallbackResolvers. This was the
// negative test for the case where Override-local-DNS was set, so the
// fallback resolvers did not need to be used. This test is still valid
// so we keep it, but the fallback test has been removed.
name: "android_does_NOT_need_fallbacks",
os: "android",
nm: &netmap.NetworkMap{
@@ -344,3 +323,48 @@ func TestDNSConfigForNetmap(t *testing.T) {
})
}
}
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
b := &LocalBackend{}
if b.allowExitNodeDNSProxyToServeName("google.com") {
t.Fatal("unexpected true on backend with nil NetMap")
}
b.netMap = &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
ExitNodeFilteredSet: []string{
".ts.net",
"some.exact.bad",
},
},
}
tests := []struct {
name string
want bool
}{
// Allow by default:
{"google.com", true},
{"GOOGLE.com", true},
// Rejected by suffix:
{"foo.TS.NET", false},
{"foo.ts.net", false},
// Suffix doesn't match
{"ts.net", true},
// Rejected by exact match:
{"some.exact.bad", false},
{"SOME.EXACT.BAD", false},
// But a prefix is okay.
{"prefix-okay.some.exact.bad", true},
}
for _, tt := range tests {
got := b.allowExitNodeDNSProxyToServeName(tt.name)
if got != tt.want {
t.Errorf("for %q = %v; want %v", tt.name, got, tt.want)
}
}
}

View File

@@ -22,11 +22,8 @@ import (
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/go-multierror/multierror"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
@@ -38,6 +35,7 @@ import (
"tailscale.com/net/dns"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
@@ -50,6 +48,7 @@ import (
"tailscale.com/types/preftype"
"tailscale.com/util/deephash"
"tailscale.com/util/dnsname"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/util/systemd"
"tailscale.com/version"
@@ -89,6 +88,7 @@ type LocalBackend struct {
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store ipn.StateStore
dialer *tsdial.Dialer // non-nil
backendLogID string
unregisterLinkMon func()
unregisterHealthWatch func()
@@ -97,9 +97,12 @@ type LocalBackend struct {
gotPortPollRes chan struct{} // closed upon first readPoller result
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
varRoot string // or empty if SetVarRoot never called
filterHash deephash.Sum
filterAtomic atomic.Value // of *filter.Filter
// The mutex protects the following elements.
mu sync.Mutex
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
@@ -122,6 +125,7 @@ type LocalBackend struct {
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
keyExpired bool
authURL string // cleared on Notify
authURLSticky string // not cleared on Notify
interact bool
@@ -138,7 +142,11 @@ type LocalBackend struct {
// same as the Network Extension lifetime and we can thus avoid
// double-copying files by writing them to the right location
// immediately.
directFileRoot string
// It's also used on Synology & TrueNAS, but in that case DoFinalRename
// is also set true, which moves the *.partial file to its final
// name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on Synology & TrueNAS
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -152,16 +160,18 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
// NewLocalBackend returns a new LocalBackend that is ready to run,
// but is not actually running.
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
//
// If dialer is nil, a new one is made.
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: wgengine must not be nil")
panic("ipn.NewLocalBackend: engine must not be nil")
}
if dialer == nil {
dialer = new(tsdial.Dialer)
}
osshare.SetFileSharingEnabled(false, logf)
// Default filter blocks everything and logs nothing, until Start() is called.
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
ctx, cancel := context.WithCancel(context.Background())
portpoll, err := portlist.NewPoller()
if err != nil {
@@ -176,11 +186,16 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
dialer: dialer,
backendLogID: logid,
state: ipn.NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
// Default filter blocks everything and logs nothing, until Start() is called.
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
b.statusChanged = sync.NewCond(&b.statusLock)
b.e.SetStatusCallback(b.setWgengineStatus)
@@ -207,6 +222,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
return b, nil
}
// Dialer returns the backend's dialer.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
}
// SetDirectFileRoot sets the directory to download files to directly,
// without buffering them through an intermediate daemon-owned
// tailcfg.UserID-specific directory.
@@ -218,6 +238,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.directFileRoot = dir
}
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
// a received "name.partial" file to "name" when the download is complete.
//
// This only applies when SetDirectFileRoot is non-empty.
// The default is false.
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileDoFinalRename = v
}
// b.mu must be held.
func (b *LocalBackend) maybePauseControlClientLocked() {
if b.cc == nil {
@@ -335,8 +366,8 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
if err := health.OverallError(); err != nil {
switch e := err.(type) {
case multierror.MultipleErrors:
for _, err := range e {
case multierr.Error:
for _, err := range e.Errors() {
s.Health = append(s.Health, err.Error())
}
default:
@@ -349,8 +380,18 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
}
})
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
if b.netMap != nil && b.netMap.SelfNode != nil {
ss.ID = b.netMap.SelfNode.StableID
if b.netMap != nil {
ss.HostName = b.netMap.Hostinfo.Hostname
ss.DNSName = b.netMap.Name
ss.UserID = b.netMap.User
if sn := b.netMap.SelfNode; sn != nil {
ss.ID = sn.StableID
if c := sn.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else {
ss.HostName, _ = os.Hostname()
}
for _, pln := range b.peerAPIListeners {
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
@@ -376,33 +417,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
if p.LastSeen != nil {
lastSeen = *p.LastSeen
}
var tailAddr4 string
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
for _, addr := range p.Addresses {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
if addr.IP().Is4() && tailAddr4 == "" {
// The peer struct previously only allowed a single
// Tailscale IP address. For compatibility for a few releases starting
// with 1.8, keep it pulled out as IPv4-only for a bit.
tailAddr4 = addr.IP().String()
}
tailscaleIPs = append(tailscaleIPs, addr.IP())
}
}
sb.AddPeer(key.NodePublicFromRaw32(mem.B(p.Key[:])), &ipnstate.PeerStatus{
InNetworkMap: true,
ID: p.StableID,
UserID: p.User,
TailAddrDeprecated: tailAddr4,
TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname,
DNSName: p.Name,
OS: p.Hostinfo.OS,
KeepAlive: p.KeepAlive,
Created: p.Created,
LastSeen: lastSeen,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
exitNodeOption := tsaddr.PrefixesContainsFunc(p.AllowedIPs, func(r netaddr.IPPrefix) bool {
return r.Bits() == 0
})
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
InNetworkMap: true,
ID: p.StableID,
UserID: p.User,
TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname,
DNSName: p.Name,
OS: p.Hostinfo.OS,
KeepAlive: p.KeepAlive,
Created: p.Created,
LastSeen: lastSeen,
Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
ExitNodeOption: exitNodeOption,
})
}
}
@@ -452,18 +490,35 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// TODO(crawshaw): display in the UI.
if errors.Is(st.Err, io.EOF) {
b.logf("[v1] Received error: EOF")
} else {
b.logf("Received error: %v", st.Err)
e := st.Err.Error()
b.send(ipn.Notify{ErrMessage: &e})
return
}
b.logf("Received error: %v", st.Err)
var uerr controlclient.UserVisibleError
if errors.As(st.Err, &uerr) {
s := uerr.UserVisibleError()
b.send(ipn.Notify{ErrMessage: &s})
}
return
}
b.mu.Lock()
wasBlocked := b.blocked
keyExpiryExtended := false
if st.NetMap != nil {
wasExpired := b.keyExpired
isExpired := !st.NetMap.Expiry.IsZero() && st.NetMap.Expiry.Before(time.Now())
if wasExpired && !isExpired {
keyExpiryExtended = true
}
b.keyExpired = isExpired
}
b.mu.Unlock()
if keyExpiryExtended && wasBlocked {
// Key extended, unblock the engine
b.blockEngineUpdates(false)
}
if st.LoginFinished != nil && wasBlocked {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
@@ -570,6 +625,11 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
// rather than by IP. It returns whether prefs was mutated.
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
if nm == nil {
// No netmap, can't resolve anything.
return false
}
// If we have a desired IP on file, try to find the corresponding
// node.
if b.prefs.ExitNodeIP.IsZero() {
@@ -847,7 +907,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
})
}
var discoPublic tailcfg.DiscoKey
var discoPublic key.DiscoPublic
if controlclient.Debug.Disco {
discoPublic = b.e.DiscoPublicKey()
}
@@ -983,20 +1043,25 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
if !haveNetmap {
b.logf("netmap packet filter: (not ready yet)")
b.e.SetFilter(filter.NewAllowNone(b.logf, logNets))
b.setFilter(filter.NewAllowNone(b.logf, logNets))
return
}
oldFilter := b.e.GetFilter()
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
} else {
b.logf("netmap packet filter: %v filters", len(packetFilter))
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
}
}
func (b *LocalBackend) setFilter(f *filter.Filter) {
b.filterAtomic.Store(f)
b.e.SetFilter(f)
}
var removeFromDefaultRoute = []netaddr.IPPrefix{
// RFC1918 LAN ranges
netaddr.MustParseIPPrefix("192.168.0.0/16"),
@@ -1224,7 +1289,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
return
}
if apiSrv != nil && apiSrv.hasFilesWaiting() {
if apiSrv.hasFilesWaiting() {
n.FilesWaiting = &empty.Message{}
}
@@ -1562,7 +1627,7 @@ func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineSt
var peerStats, peerKeys strings.Builder
ret.LiveDERPs = s.DERPs
ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{}
ret.LivePeers = map[key.NodePublic]ipnstate.PeerStatusLite{}
for _, p := range s.Peers {
if !p.LastHandshake.IsZero() {
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
@@ -1633,7 +1698,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
}
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
// unlocks b.mu when done.
// unlocks b.mu when done. newp ownership passes to this function.
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
netMap := b.netMap
stateKey := b.stateKey
@@ -1641,6 +1706,10 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
oldp := b.prefs
newp.Persist = oldp.Persist // caller isn't allowed to override this
b.prefs = newp
// findExitNodeIDLocked returns whether it updated b.prefs, but
// everything in this function treats b.prefs as completely new
// anyway. No-op if no exit node resolution is needed.
b.findExitNodeIDLocked(netMap)
b.inServerMode = newp.ForceDaemon
// We do this to avoid holding the lock while doing everything else.
newp = b.prefs.Clone()
@@ -1676,7 +1745,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
// notified (to update its prefs/persist) on
// account switch. Log this while we figure it
// out.
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
b.logf("active login: %q ([unexpected] corp#461, not %q)", newp.Persist.LoginName, login)
}
}
}
@@ -1718,15 +1787,24 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
for _, pln := range b.peerAPIListeners {
proto := tailcfg.ServiceProto("peerapi4")
proto := tailcfg.PeerAPI4
if pln.ip.Is6() {
proto = "peerapi6"
proto = tailcfg.PeerAPI6
}
ret = append(ret, tailcfg.Service{
Proto: proto,
Port: uint16(pln.port),
})
}
switch runtime.GOOS {
case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
// These are the platforms currently supported by
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
ret = append(ret, tailcfg.Service{
Proto: tailcfg.PeerAPIDNS,
Port: 1, // version
})
}
return ret
}
@@ -1771,6 +1849,12 @@ func (b *LocalBackend) NetMap() *netmap.NetworkMap {
return b.netMap
}
func (b *LocalBackend) isEngineBlocked() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.blocked
}
// blockEngineUpdate sets b.blocked to block, while holding b.mu. Its
// indirect effect is to turn b.authReconfig() into a no-op if block
// is true.
@@ -1821,6 +1905,15 @@ func (b *LocalBackend) authReconfig() {
}
}
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
if err != nil {
b.logf("wgcfg: %v", err)
@@ -1919,12 +2012,32 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
return dcfg
}
for _, dom := range nm.DNS.Domains {
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
logf("[unexpected] non-FQDN search domain %q", dom)
}
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
}
if nm.DNS.Proxied { // actually means "enable MagicDNS"
for _, dom := range magicDNSRootDomains(nm) {
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
}
}
addDefault := func(resolvers []dnstype.Resolver) {
for _, r := range resolvers {
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
}
}
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it.
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
addDefault([]dnstype.Resolver{{Addr: dohURL}})
return dcfg
}
addDefault(nm.DNS.Resolvers)
for suffix, resolvers := range nm.DNS.Routes {
fqdn, err := dnsname.ToFQDN(suffix)
@@ -1946,18 +2059,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
}
}
for _, dom := range nm.DNS.Domains {
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
logf("[unexpected] non-FQDN search domain %q", dom)
}
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
}
if nm.DNS.Proxied { // actually means "enable MagicDNS"
for _, dom := range magicDNSRootDomains(nm) {
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
}
}
// Set FallbackResolvers as the default resolvers in the
// scenarios that can't handle a purely split-DNS config. See
@@ -1981,9 +2082,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
addDefault(nm.DNS.FallbackResolvers)
case len(dcfg.Routes) == 0:
// No settings requiring split DNS, no problem.
case versionOS == "android":
// We don't support split DNS at all on Android yet.
addDefault(nm.DNS.FallbackResolvers)
}
return dcfg
@@ -1999,34 +2097,29 @@ func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver {
return cfg
}
// SetVarRoot sets the root directory of Tailscale's writable
// storage area . (e.g. "/var/lib/tailscale")
//
// It should only be called before the LocalBackend is used.
func (b *LocalBackend) SetVarRoot(dir string) {
b.varRoot = dir
}
// TailscaleVarRoot returns the root directory of Tailscale's writable
// storage area. (e.g. "/var/lib/tailscale")
//
// It returns an empty string if there's no configured or discovered
// location.
func (b *LocalBackend) TailscaleVarRoot() string {
if b.varRoot != "" {
return b.varRoot
}
switch runtime.GOOS {
case "ios", "android":
dir, _ := paths.AppSharedDir.Load().(string)
return dir
}
// Temporary (2021-09-27) transitional fix for #2927 (Synology
// cert dir) on the way towards a more complete fix
// (#2932). It fixes any case where the state file is provided
// to tailscaled explicitly when it's not in the default
// location.
if fs, ok := b.store.(*ipn.FileStore); ok {
if fp := fs.Path(); fp != "" {
if dir := filepath.Dir(fp); strings.EqualFold(filepath.Base(dir), "tailscale") {
return dir
}
}
}
stateFile := paths.DefaultTailscaledStateFile()
if stateFile == "" {
return ""
}
return filepath.Dir(stateFile)
return ""
}
func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
@@ -2035,7 +2128,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
}
varRoot := b.TailscaleVarRoot()
if varRoot == "" {
b.logf("peerapi disabled; no state directory")
b.logf("Taildrop disabled; no state directory")
return ""
}
baseDir := fmt.Sprintf("%s-uid-%d",
@@ -2043,7 +2136,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
uid)
dir := filepath.Join(varRoot, "files", baseDir)
if err := os.MkdirAll(dir, 0700); err != nil {
b.logf("peerapi disabled; error making directory: %v", err)
b.logf("Taildrop disabled; error making directory: %v", err)
return ""
}
return dir
@@ -2106,22 +2199,20 @@ func (b *LocalBackend) initPeerAPIListener() {
fileRoot := b.fileRootLocked(selfNode.User)
if fileRoot == "" {
return
}
var tunName string
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
if tunWrap, _, ok := ge.GetInternals(); ok {
tunName, _ = tunWrap.Name()
}
b.logf("peerapi starting without Taildrop directory configured")
}
ps := &peerAPIServer{
b: b,
rootDir: fileRoot,
tunName: tunName,
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
b: b,
rootDir: fileRoot,
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
}
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok {
ps.resolver = r
}
}
b.peerAPIServer = ps
@@ -2398,6 +2489,7 @@ func (b *LocalBackend) nextState() ipn.State {
wantRunning = b.prefs.WantRunning
loggedOut = b.prefs.LoggedOut
st = b.engineStatus
keyExpired = b.keyExpired
)
b.mu.Unlock()
@@ -2430,7 +2522,9 @@ func (b *LocalBackend) nextState() ipn.State {
}
case !wantRunning:
return ipn.Stopped
case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0:
case keyExpired:
// NetMap must be non-nil for us to get here.
// The node key expired, need to relogin.
return ipn.NeedsLogin
case netMap.MachineStatus != tailcfg.MachineAuthorized:
// TODO(crawshaw): handle tailcfg.MachineInvalid
@@ -2511,6 +2605,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.userID = ""
b.setNetMapLocked(nil)
b.prefs = new(ipn.Prefs)
b.keyExpired = false
b.authURL = ""
b.authURLSticky = ""
b.activeLogin = ""
@@ -2595,6 +2690,7 @@ func hasCapability(nm *netmap.NetworkMap, cap string) bool {
}
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.dialer.SetNetMap(nm)
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
@@ -2680,7 +2776,7 @@ func (b *LocalBackend) OperatorUserID() string {
// TestOnlyPublicKeys returns the current machine and node public
// keys. Used in tests only to facilitate automated node authorization
// in the test harness.
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey tailcfg.NodeKey) {
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeKey key.NodePublic) {
b.mu.Lock()
prefs := b.prefs
machinePrivKey := b.machinePrivKey
@@ -2692,16 +2788,13 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK
mk := machinePrivKey.Public()
nk := prefs.Persist.PrivateNodeKey.Public()
return mk, tailcfg.NodeKeyFromNodePublic(nk)
return mk, nk
}
func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return nil, errors.New("peerapi disabled")
}
return apiSrv.WaitingFiles()
}
@@ -2709,9 +2802,6 @@ func (b *LocalBackend) DeleteFile(name string) error {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return errors.New("peerapi disabled")
}
return apiSrv.DeleteFile(name)
}
@@ -2719,9 +2809,6 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return nil, 0, errors.New("peerapi disabled")
}
return apiSrv.OpenFile(name)
}
@@ -2782,7 +2869,7 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
b.mu.Lock()
cc := b.cc
if prefs := b.prefs; prefs != nil {
req.NodeKey = tailcfg.NodeKeyFromNodePublic(prefs.Persist.PrivateNodeKey.Public())
req.NodeKey = prefs.Persist.PrivateNodeKey.Public()
}
b.mu.Unlock()
if cc == nil {
@@ -2835,9 +2922,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
var p4, p6 uint16
for _, s := range peer.Hostinfo.Services {
switch s.Proto {
case "peerapi4":
case tailcfg.PeerAPI4:
p4 = s.Port
case "peerapi6":
case tailcfg.PeerAPI6:
p6 = s.Port
}
}
@@ -2871,48 +2958,97 @@ func (b *LocalBackend) CheckIPForwarding() error {
if wgengine.IsNetstackRouter(b.e) {
return nil
}
if isBSD(runtime.GOOS) {
switch {
case isBSD(runtime.GOOS):
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
case runtime.GOOS == "linux":
return checkIPForwardingLinux()
default:
// TODO: subnet routing and exit nodes probably don't work
// correctly on non-linux, non-netstack OSes either. Warn
// instead of being silent?
return nil
}
}
// checkIPForwardingLinux checks if IP forwarding is enabled correctly
// for subnet routing and exit node functionality. Returns an error
// describing configuration issues if the configuration is not
// definitely good.
func checkIPForwardingLinux() error {
const kbLink = "\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
disabled, err := disabledSysctls("net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
if err != nil {
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
var keys []string
if runtime.GOOS == "linux" {
keys = append(keys, "net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
} else if isBSD(runtime.GOOS) {
keys = append(keys, "net.inet.ip.forwarding")
} else {
if len(disabled) == 0 {
// IP forwarding is enabled systemwide, all is well.
return nil
}
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
for _, key := range keys {
bs, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
// IP forwarding isn't enabled globally, but it might be enabled
// on a per-interface basis. Check if it's on for all interfaces,
// and warn appropriately if it's not.
ifaces, err := interfaces.GetList()
if err != nil {
return fmt.Errorf("Couldn't enumerate network interfaces, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
var (
warnings []string
anyEnabled bool
)
for _, iface := range ifaces {
if iface.Name == "lo" {
continue
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
disabled, err = disabledSysctls(fmt.Sprintf("net.ipv4.conf.%s.forwarding", iface.Name), fmt.Sprintf("net.ipv6.conf.%s.forwarding", iface.Name))
if err != nil {
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix)
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if !on {
return fmt.Errorf("%s is disabled.%s", key, suffix)
if len(disabled) > 0 {
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, strings.Join(disabled, ", ")))
} else {
anyEnabled = true
}
}
if !anyEnabled {
// IP forwarding is compeltely disabled, just say that rather
// than enumerate all the interfaces on the system.
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink)
}
if len(warnings) > 0 {
// If partially enabled, enumerate the bits that won't work.
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink)
}
return nil
}
// peerDialControlFunc is non-nil on platforms that require a way to
// bind to dial out to other peers.
var peerDialControlFunc func(*LocalBackend) func(network, address string, c syscall.RawConn) error
// PeerDialControlFunc returns a net.Dialer.Control func (possibly nil) to use to
// dial other Tailscale peers from the current environment.
func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
if peerDialControlFunc != nil {
return peerDialControlFunc(b)
// disabledSysctls checks if the given sysctl keys are off, according
// to strconv.ParseBool. Returns a list of keys that are disabled, or
// err if something went wrong which prevented the lookups from
// completing.
func disabledSysctls(sysctls ...string) (disabled []string, err error) {
for _, k := range sysctls {
// TODO: on linux, we can get at these values via /proc/sys,
// rather than fork subcommands that may not be installed.
bs, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return nil, fmt.Errorf("couldn't check %s (%v)", k, err)
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil {
return nil, fmt.Errorf("couldn't parse %s (%v)", k, err)
}
if !on {
disabled = append(disabled, k)
}
}
return nil
return disabled, nil
}
// DERPMap returns the current DERPMap in use, or nil if not connected.
@@ -2924,3 +3060,77 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
}
return b.netMap.DERPMap
}
// OfferingExitNode reports whether b is currently offering exit node
// access.
func (b *LocalBackend) OfferingExitNode() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
var def4, def6 bool
for _, r := range b.prefs.AdvertiseRoutes {
if r.Bits() != 0 {
continue
}
if r.IP().Is4() {
def4 = true
} else if r.IP().Is6() {
def6 = true
}
}
return def4 && def6
}
// allowExitNodeDNSProxyToServeName reports whether the Exit Node DNS
// proxy is allowed to serve responses for the provided DNS name.
func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.netMap
if nm == nil {
return false
}
name = strings.ToLower(name)
for _, bad := range nm.DNS.ExitNodeFilteredSet {
if bad == "" {
// Invalid, ignore.
continue
}
if bad[0] == '.' {
// Entries beginning with a dot are suffix matches.
if dnsname.HasSuffix(name, bad) {
return false
}
continue
}
// Otherwise entries are exact matches. They're
// guaranteed to be lowercase already.
if name == bad {
return false
}
}
return true
}
// exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
// to exitNodeID's DoH service, if available.
//
// If exitNodeID is the zero valid, it returns "", false.
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
if exitNodeID.IsZero() {
return "", false
}
for _, p := range nm.Peers {
if p.StableID != exitNodeID {
continue
}
for _, s := range p.Hostinfo.Services {
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
return peerAPIBase(nm, p) + "/dns-query", true
}
}
}
return "", false
}

View File

@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
},
{
"Node names identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
true,
},
{
"Node names differ",
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
false,
},
{
@@ -117,8 +117,8 @@ func TestNetworkMapCompare(t *testing.T) {
{
"Node Users differ",
// User field is not checked.
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
true,
},
}
@@ -445,7 +445,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(eng.Close)
lb, err := NewLocalBackend(logf, "logid", store, eng)
lb, err := NewLocalBackend(logf, "logid", store, nil, eng)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}

View File

@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
}
t.Cleanup(e.Close)
lb, err := NewLocalBackend(logf, idA.String(), store, e)
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e)
if err != nil {
t.Fatal(err)
}
@@ -90,7 +90,7 @@ func TestLocalLogLines(t *testing.T) {
TxBytes: 10,
RxBytes: 10,
LastHandshake: time.Now(),
NodeKey: tailcfg.NodeKeyFromNodePublic(key.NewNode().Public()),
NodeKey: key.NewNode().Public(),
}},
})
lb.mu.Unlock()
@@ -105,7 +105,7 @@ func TestLocalLogLines(t *testing.T) {
TxBytes: 11,
RxBytes: 12,
LastHandshake: time.Now(),
NodeKey: tailcfg.NodeKeyFromNodePublic(key.NewNode().Public()),
NodeKey: key.NewNode().Public(),
}},
})
lb.mu.Unlock()

View File

@@ -6,6 +6,8 @@ package ipnlocal
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash/crc32"
@@ -27,32 +29,48 @@ import (
"unicode"
"unicode/utf8"
"golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
// addH2C is non-nil on platforms where we want to add H2C
// ("cleartext" HTTP/2) support to the peerAPI.
var addH2C func(*http.Server)
type peerAPIServer struct {
b *LocalBackend
rootDir string
tunName string
rootDir string // empty means file receiving unavailable
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
resolver *resolver.Resolver
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on GUI macOS version.
// somewhere itself. This is used on the GUI macOS versions
// and on Synology.
// In directFileMode, the peerapi doesn't do the final rename
// from "foo.jpg.partial" to "foo.jpg".
// from "foo.jpg.partial" to "foo.jpg" unless
// directFileDoFinalRename is set.
directFileMode bool
// directFileDoFinalRename is whether in directFileMode we
// additionally move the *.direct file to its final name after
// it's received.
directFileDoFinalRename bool
}
const (
@@ -69,6 +87,10 @@ const (
deletedSuffix = ".deleted"
)
func (s *peerAPIServer) canReceiveFiles() bool {
return s != nil && s.rootDir != ""
}
func validFilenameRune(r rune) bool {
switch r {
case '/':
@@ -115,7 +137,7 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
// hasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *peerAPIServer) hasFilesWaiting() bool {
if s.rootDir == "" || s.directFileMode {
if s == nil || s.rootDir == "" || s.directFileMode {
return false
}
if s.knownEmpty.Get() {
@@ -175,8 +197,11 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s == nil {
return nil, errNilPeerAPIServer
}
if s.rootDir == "" {
return nil, errors.New("peerapi disabled; no storage configured")
return nil, errNoTaildrop
}
if s.directFileMode {
return nil, nil
@@ -240,6 +265,11 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
return ret, nil
}
var (
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
)
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
// it failed earlier. This happens on Windows when various anti-virus
// tools hook into filesystem operations and have the file open still
@@ -255,8 +285,11 @@ func tryDeleteAgain(fullPath string) {
}
func (s *peerAPIServer) DeleteFile(baseName string) error {
if s == nil {
return errNilPeerAPIServer
}
if s.rootDir == "" {
return errors.New("peerapi disabled; no storage configured")
return errNoTaildrop
}
if s.directFileMode {
return errors.New("deletes not allowed in direct mode")
@@ -321,8 +354,11 @@ func touchFile(path string) error {
}
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s == nil {
return nil, 0, errNilPeerAPIServer
}
if s.rootDir == "" {
return nil, 0, errors.New("peerapi disabled; no storage configured")
return nil, 0, errNoTaildrop
}
if s.directFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
@@ -355,7 +391,7 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
// On iOS/macOS, this sets the lc.Control hook to
// setsockopt the interface index to bind to, to get
// out of the network sandbox.
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
if err := initListenConfig(&lc, ip, ifState, s.b.dialer.TUNName()); err != nil {
return nil, err
}
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
@@ -460,6 +496,9 @@ func (pln *peerAPIListener) serve() {
httpServer := &http.Server{
Handler: h,
}
if addH2C != nil {
addH2C(httpServer)
}
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
}
}
@@ -500,9 +539,20 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handlePeerPut(w, r)
return
}
if r.URL.Path == "/v0/goroutines" {
if strings.HasPrefix(r.URL.Path, "/dns-query") {
h.handleDNSQuery(w, r)
return
}
switch r.URL.Path {
case "/v0/goroutines":
h.handleServeGoroutines(w, r)
return
case "/v0/env":
h.handleServeEnv(w, r)
return
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html>
@@ -589,7 +639,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
return
}
if h.ps.rootDir == "" {
http.Error(w, "no rootdir", http.StatusInternalServerError)
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
return
}
rawPath := r.URL.EscapedPath()
@@ -661,7 +711,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.directFileMode {
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
@@ -710,3 +760,247 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re
}
w.Write(buf)
}
func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
var data struct {
Hostinfo *tailcfg.Hostinfo
Uid int
Args []string
Env []string
}
data.Hostinfo = hostinfo.New()
data.Uid = os.Getuid()
data.Args = os.Args
data.Env = os.Environ()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it
// without further checks.
return true
}
b := h.ps.b
if !b.OfferingExitNode() {
// If we're not an exit node, there's no point to
// being a DNS server for somebody.
return false
}
if !h.remoteAddr.IsValid() {
// This should never be the case if the peerAPIHandler
// was wired up correctly, but just in case.
return false
}
// Otherwise, we're an exit node but the peer is not us, so
// we need to check if they're allowed access to the internet.
// As peerapi bypasses wgengine/filter checks, we need to check
// ourselves. As a proxy for autogroup:internet access, we see
// if we would've accepted a packet to 0.0.0.0:53. We treat
// the IP 0.0.0.0 as being "the internet".
f, ok := b.filterAtomic.Load().(*filter.Filter)
if !ok {
return false
}
// Note: we check TCP here because the Filter type already had
// a CheckTCP method (for unit tests), but it's pretty
// arbitrary. DNS runs over TCP and UDP, so sure... we check
// TCP.
dstIP := netaddr.IPv4(0, 0, 0, 0)
remoteIP := h.remoteAddr.IP()
if remoteIP.Is6() {
// autogroup:internet for IPv6 is defined to start with 2000::/3,
// so use 2000::0 as the probe "the internet" address.
dstIP = netaddr.MustParseIP("2000::")
}
verdict := f.CheckTCP(remoteIP, dstIP, 53)
return verdict == filter.Accept
}
// handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
// It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
if h.ps.resolver == nil {
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
return
}
if !h.replyToDNSQueries() {
http.Error(w, "DNS access denied", http.StatusForbidden)
return
}
pretty := false // non-DoH debug mode for humans
q, publicError := dohQuery(r)
if publicError != "" && r.Method == "GET" {
if name := r.FormValue("q"); name != "" {
pretty = true
publicError = ""
q = dnsQueryForName(name, r.FormValue("t"))
}
}
if publicError != "" {
http.Error(w, publicError, http.StatusBadRequest)
return
}
// Some timeout that's short enough to be noticed by humans
// but long enough that it's longer than real DNS timeouts.
const arbitraryTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
defer cancel()
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr, h.ps.b.allowExitNodeDNSProxyToServeName)
if err != nil {
h.logf("handleDNS fwd error: %v", err)
if err := ctx.Err(); err != nil {
http.Error(w, err.Error(), 500)
} else {
http.Error(w, "DNS forwarding error", 500)
}
return
}
if pretty {
// Non-standard response for interactive debugging.
w.Header().Set("Content-Type", "application/json")
writePrettyDNSReply(w, res)
return
}
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Content-Length", strconv.Itoa(len(res)))
w.Write(res)
}
func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
const maxQueryLen = 256 << 10
switch r.Method {
default:
return nil, "bad HTTP method"
case "GET":
q64 := r.FormValue("dns")
if q64 == "" {
return nil, "missing 'dns' parameter"
}
if base64.RawURLEncoding.DecodedLen(len(q64)) > maxQueryLen {
return nil, "query too large"
}
q, err := base64.RawURLEncoding.DecodeString(q64)
if err != nil {
return nil, "invalid 'dns' base64 encoding"
}
return q, ""
case "POST":
if r.Header.Get("Content-Type") != "application/dns-message" {
return nil, "unexpected Content-Type"
}
q, err := io.ReadAll(io.LimitReader(r.Body, maxQueryLen+1))
if err != nil {
return nil, "error reading post body with DNS query"
}
if len(q) > maxQueryLen {
return nil, "query too large"
}
return q, ""
}
}
func dnsQueryForName(name, typStr string) []byte {
typ := dnsmessage.TypeA
switch strings.ToLower(typStr) {
case "aaaa":
typ = dnsmessage.TypeAAAA
case "txt":
typ = dnsmessage.TypeTXT
}
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
OpCode: 0, // query
RecursionDesired: true,
ID: 0,
})
if !strings.HasSuffix(name, ".") {
name += "."
}
b.StartQuestions()
b.Question(dnsmessage.Question{
Name: dnsmessage.MustNewName(name),
Type: typ,
Class: dnsmessage.ClassINET,
})
msg, _ := b.Finish()
return msg
}
func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
defer func() {
if err != nil {
j, _ := json.Marshal(struct {
Error string
}{err.Error()})
j = append(j, '\n')
w.Write(j)
return
}
}()
var p dnsmessage.Parser
hdr, err := p.Start(res)
if err != nil {
return err
}
if hdr.RCode != dnsmessage.RCodeSuccess {
return fmt.Errorf("DNS RCode = %v", hdr.RCode)
}
if err := p.SkipAllQuestions(); err != nil {
return err
}
var gotIPs []string
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return err
}
if h.Class != dnsmessage.ClassINET {
continue
}
switch h.Type {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, net.IP(r.A[:]).String())
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, net.IP(r.AAAA[:]).String())
case dnsmessage.TypeTXT:
r, err := p.TXTResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, r.TXT...)
}
}
j, _ := json.Marshal(gotIPs)
j = append(j, '\n')
w.Write(j)
return nil
}

View File

@@ -0,0 +1,22 @@
// 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 ipnlocal
import (
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func init() {
addH2C = func(s *http.Server) {
h2s := &http2.Server{}
s.Handler = h2c.NewHandler(s.Handler, h2s)
}
}

View File

@@ -9,10 +9,8 @@
package ipnlocal
import (
"errors"
"fmt"
"net"
"syscall"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
@@ -21,7 +19,6 @@ import (
func init() {
initListenConfig = initListenConfigNetworkExtension
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
// initListenConfigNetworkExtension configures nc for listening on IP
@@ -34,24 +31,3 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
}
return netns.SetListenConfigInterfaceIndex(nc, tunIf.Index)
}
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
b.mu.Lock()
defer b.mu.Unlock()
st := b.prevIfState
pas := b.peerAPIServer
index := -1
if st != nil && pas != nil && pas.tunName != "" {
if tunIf, ok := st.Interface[pas.tunName]; ok {
index = tunIf.Index
}
}
var lc net.ListenConfig
netns.SetListenConfigInterfaceIndex(&lc, index)
return func(network, address string, c syscall.RawConn) error {
if index == -1 {
return errors.New("failed to find TUN interface to bind to")
}
return lc.Control(network, address, c)
}
}

View File

@@ -19,8 +19,13 @@ import (
"strings"
"testing"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
type peerAPITestEnv struct {
@@ -174,7 +179,7 @@ func TestHandlePeerAPI(t *testing.T) {
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusInternalServerError),
bodyContains("no rootdir"),
bodyContains("Taildrop disabled; no storage directory"),
),
},
{
@@ -568,3 +573,55 @@ func TestDeletedMarkers(t *testing.T) {
}
}
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
var h peerAPIHandler
h.isSelf = true
if !h.replyToDNSQueries() {
t.Errorf("for isSelf = false; want true")
}
h.isSelf = false
h.remoteAddr = netaddr.MustParseIPPort("100.150.151.152:12345")
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
h.ps = &peerAPIServer{
b: &LocalBackend{
e: eng,
},
}
if h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly offering exit node")
}
h.ps.b.prefs = &ipn.Prefs{
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
}
if !h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly not offering exit node")
}
if h.replyToDNSQueries() {
t.Errorf("unexpectedly doing DNS without filter")
}
h.ps.b.setFilter(filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)))
if h.replyToDNSQueries() {
t.Errorf("unexpectedly doing DNS without filter")
}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.replyToDNSQueries() {
t.Errorf("unexpectedly deny; wanted to be a DNS server")
}
// Also test IPv6.
h.remoteAddr = netaddr.MustParseIPPort("[fe70::1]:12345")
if !h.replyToDNSQueries() {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
}
}

View File

@@ -284,7 +284,7 @@ func TestStateMachine(t *testing.T) {
t.Cleanup(e.Close)
cc := newMockControl(t)
b, err := NewLocalBackend(logf, "logid", store, e)
b, err := NewLocalBackend(logf, "logid", store, nil, e)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
@@ -867,6 +867,45 @@ func TestStateMachine(t *testing.T) {
// change either.
c.Assert(ipn.Starting, qt.Equals, b.State())
}
t.Logf("\n\nExpireKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(-time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(1)
cc.assertCalls("unpause", "unpause")
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
c.Assert(b.isEngineBlocked(), qt.IsTrue)
}
t.Logf("\n\nExtendKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(1)
cc.assertCalls("unpause", "unpause", "unpause")
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
c.Assert(ipn.Starting, qt.Equals, b.State())
c.Assert(b.isEngineBlocked(), qt.IsFalse)
}
notifies.expect(1)
// Fake a DERP connection.
b.setWgengineStatus(&wgengine.Status{DERPs: 1}, nil)
{
nn := notifies.drain(1)
cc.assertCalls("unpause")
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.Running, qt.Equals, *nn[0].State)
c.Assert(ipn.Running, qt.Equals, b.State())
}
}
type testStateStorage struct {
@@ -902,7 +941,7 @@ func TestWGEngineStatusRace(t *testing.T) {
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
c.Assert(err, qt.IsNil)
t.Cleanup(eng.Close)
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), eng)
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng)
c.Assert(err, qt.IsNil)
cc := newMockControl(t)

View File

@@ -35,9 +35,9 @@ import (
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/ipn/store/aws"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
@@ -48,20 +48,18 @@ import (
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
)
// Options is the configuration of the Tailscale node agent.
type Options struct {
// SocketPath, on unix systems, is the unix socket path to listen
// on for frontend connections.
SocketPath string
// Port, on windows, is the localhost TCP port to listen on for
// frontend connections.
Port int
// StatePath is the path to the stored agent state.
StatePath string
// VarRoot is the the Tailscale daemon's private writable
// directory (usually "/var/lib/tailscale" on Linux) that
// contains the "tailscaled.state" file, the "certs" directory
// for TLS certs, and the "files" directory for incoming
// Taildrop files before they're moved to a user directory.
// If empty, Taildrop and TLS certs don't function.
VarRoot string
// AutostartStateKey, if non-empty, immediately starts the agent
// using the given StateKey. If empty, the agent stays idle and
@@ -83,10 +81,6 @@ type Options struct {
// the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0.
SurviveDisconnects bool
// DebugMux, if non-nil, specifies an HTTP ServeMux in which
// to register a debug handler.
DebugMux *http.ServeMux
}
// Server is an IPN backend and its set of 0 or more active localhost
@@ -100,8 +94,8 @@ type Server struct {
// being run in "client mode" that requires an active GUI
// connection (such as on Windows by default). Even if this
// is true, the ForceDaemon pref can override this.
resetOnZero bool
opts Options
resetOnZero bool
autostartStateKey ipn.StateKey
bsMu sync.Mutex // lock order: bsMu, then mu
bs *ipn.BackendServer
@@ -114,6 +108,9 @@ type Server struct {
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
}
// LocalBackend returns the server's LocalBackend.
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b }
// connIdentity represents the owner of a localhost TCP or unix socket connection.
type connIdentity struct {
Conn net.Conn
@@ -414,13 +411,16 @@ func (s *Server) checkConnIdentityLocked(ci connIdentity) error {
//
// s.mu must not be held.
func (s *Server) localAPIPermissions(ci connIdentity) (read, write bool) {
if runtime.GOOS == "windows" {
switch runtime.GOOS {
case "windows":
s.mu.Lock()
defer s.mu.Unlock()
if s.checkConnIdentityLocked(ci) == nil {
return true, true
}
return false, false
case "js":
return true, true
}
if ci.IsUnixSock {
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
@@ -606,18 +606,57 @@ func tryWindowsAppDataMigration(logf logger.Logf, path string) string {
return paths.TryConfigFileMigration(logf, oldFile, path)
}
// StateStore returns a StateStore from path.
//
// The path should be an absolute path to a file.
//
// Special cases:
//
// * empty string means to use an in-memory store
// * if the string begins with "kube:", the suffix
// is a Kubernetes secret name
// * if the string begins with "arn:", the value is
// an AWS ARN for an SSM.
func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
if path == "" {
return &ipn.MemoryStore{}, nil
}
const kubePrefix = "kube:"
const arnPrefix = "arn:"
switch {
case strings.HasPrefix(path, kubePrefix):
secretName := strings.TrimPrefix(path, kubePrefix)
store, err := ipn.NewKubeStore(secretName)
if err != nil {
return nil, fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
}
return store, nil
case strings.HasPrefix(path, arnPrefix):
store, err := aws.NewStore(path)
if err != nil {
return nil, fmt.Errorf("aws.NewStore(%q): %v", path, err)
}
return store, nil
}
if runtime.GOOS == "windows" {
path = tryWindowsAppDataMigration(logf, path)
}
store, err := ipn.NewFileStore(path)
if err != nil {
return nil, fmt.Errorf("ipn.NewFileStore(%q): %v", path, err)
}
return store, nil
}
// Run runs a Tailscale backend service.
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
//
// Deprecated: use New and Server.Run instead.
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
getEngine = getEngineUntilItWorksWrapper(getEngine)
runDone := make(chan struct{})
defer close(runDone)
listen, _, err := safesocket.Listen(opts.SocketPath, uint16(opts.Port))
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
var serverMu sync.Mutex
var serverOrNil *Server
@@ -633,57 +672,28 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
s.stopAll()
}
serverMu.Unlock()
listen.Close()
ln.Close()
}()
logf("Listening on %v", listen.Addr())
logf("Listening on %v", ln.Addr())
var serverModeUser *user.User
var store ipn.StateStore
if opts.StatePath != "" {
const kubePrefix = "kube:"
const arnPrefix = "arn:"
path := opts.StatePath
switch {
case strings.HasPrefix(path, kubePrefix):
secretName := strings.TrimPrefix(path, kubePrefix)
store, err = ipn.NewKubeStore(secretName)
if err != nil {
return fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
}
case strings.HasPrefix(path, arnPrefix):
store, err = aws.NewStore(path)
if err != nil {
return fmt.Errorf("aws.NewStore(%q): %v", path, err)
}
default:
if runtime.GOOS == "windows" {
path = tryWindowsAppDataMigration(logf, path)
}
store, err = ipn.NewFileStore(path)
if err != nil {
return fmt.Errorf("ipn.NewFileStore(%q): %v", path, err)
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return fmt.Errorf("calling ReadState on state store: %w", err)
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return fmt.Errorf("calling ReadState on %s: %w", path, err)
}
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := lookupUserFromID(logf, uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := lookupUserFromID(logf, uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
}
} else {
store = &ipn.MemoryStore{}
}
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
@@ -693,7 +703,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
c, err := ln.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
@@ -720,62 +730,107 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}
}
if unservedConn != nil {
listen = &listenerWithReadyConn{
Listener: listen,
ln = &listenerWithReadyConn{
Listener: ln,
c: unservedConn,
}
}
server, err := New(logf, logid, store, eng, serverModeUser, opts)
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
if err != nil {
return err
}
serverMu.Lock()
serverOrNil = server
serverMu.Unlock()
return server.Serve(ctx, listen)
return server.Run(ctx, ln)
}
// New returns a new Server.
//
// The opts.StatePath option is ignored; it's only used by Run.
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, serverModeUser *user.User, opts Options) (*Server, error) {
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
// To start it, use the Server.Run method.
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, serverModeUser *user.User, opts Options) (*Server, error) {
b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
if err != nil {
return nil, fmt.Errorf("NewLocalBackend: %v", err)
}
b.SetVarRoot(opts.VarRoot)
b.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
})
if opts.DebugMux != nil {
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveHTMLStatus(w, b)
})
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
if err != nil {
logf("%s Taildrop support: %v", dg, err)
} else {
logf("%s Taildrop: using %v", dg, path)
b.SetDirectFileRoot(path)
b.SetDirectFileDoFinalRename(true)
}
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return nil, fmt.Errorf("calling ReadState on store: %w", err)
}
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := lookupUserFromID(logf, uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
}
}
server := &Server{
b: b,
backendLogID: logid,
logf: logf,
resetOnZero: !opts.SurviveDisconnects,
serverModeUser: serverModeUser,
opts: opts,
b: b,
backendLogID: logid,
logf: logf,
resetOnZero: !opts.SurviveDisconnects,
serverModeUser: serverModeUser,
autostartStateKey: opts.AutostartStateKey,
}
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
return server, nil
}
// Serve accepts connections from ln forever.
// Run runs the server, accepting connections from ln forever.
//
// The context is only used to suppress errors
func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
// If the context is done, the listener is closed.
func (s *Server) Run(ctx context.Context, ln net.Listener) error {
defer s.b.Shutdown()
if s.opts.AutostartStateKey != "" {
runDone := make(chan struct{})
defer close(runDone)
// When the context is closed or when we return, whichever is first, close our listener
// and all open connections.
go func() {
select {
case <-ctx.Done():
case <-runDone:
}
s.stopAll()
ln.Close()
}()
if s.autostartStateKey != "" {
s.bs.GotCommand(ctx, &ipn.Command{
Version: version.Long,
Start: &ipn.StartArgs{
Opts: ipn.Options{StateKey: s.opts.AutostartStateKey},
Opts: ipn.Options{StateKey: s.autostartStateKey},
},
})
}
@@ -813,14 +868,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
panic("cannot determine executable: " + err.Error())
}
if runtime.GOOS == "windows" {
if len(args) != 2 && args[0] != "/subproc" {
panic(fmt.Sprintf("unexpected arguments %q", args))
}
logID := args[1]
logf = filelogger.New("tailscale-service", logID, logf)
}
var proc struct {
mu sync.Mutex
p *os.Process
@@ -1013,13 +1060,13 @@ func (s *Server) localhostHandler(ci connIdentity) http.Handler {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
return
}
serveHTMLStatus(w, s.b)
s.ServeHTMLStatus(w, r)
})
}
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
st := s.b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
}
@@ -1074,3 +1121,50 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
}
return ln.Listener.Accept()
}
func findTaildropDir(dg distro.Distro) (string, error) {
const name = "Taildrop"
switch dg {
case distro.Synology:
return findSynologyTaildropDir(name)
case distro.TrueNAS:
return findTrueNASTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
// findSynologyTaildropDir looks for the first volume containing a
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
// but on DSM7 at least, we lack permissions to run that.
func findSynologyTaildropDir(name string) (dir string, err error) {
for i := 1; i <= 16; i++ {
dir = fmt.Sprintf("/volume%v/%s", i, name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findTrueNASTaildropDir returns the first matching directory of
// /mnt/{name} or /mnt/*/{name}
func findTrueNASTaildropDir(name string) (dir string, err error) {
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
dir = fmt.Sprintf("/mnt/%s", name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := ioutil.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
for _, fi := range fis {
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@@ -13,6 +13,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnserver"
"tailscale.com/net/tsdial"
"tailscale.com/safesocket"
"tailscale.com/wgengine"
)
@@ -32,10 +33,11 @@ func TestRunMultipleAccepts(t *testing.T) {
t.Logf(format, args...)
}
s := safesocket.DefaultConnectionStrategy(socketPath)
connect := func() {
for i := 1; i <= 2; i++ {
logf("connect %d ...", i)
c, err := safesocket.Connect(socketPath, 0)
c, err := safesocket.Connect(s)
if err != nil {
t.Fatalf("safesocket.Connect: %v\n", err)
}
@@ -62,10 +64,16 @@ func TestRunMultipleAccepts(t *testing.T) {
}
t.Cleanup(eng.Close)
opts := ipnserver.Options{
SocketPath: socketPath,
}
opts := ipnserver.Options{}
t.Logf("pre-Run")
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", ipnserver.FixedEngine(eng), opts)
store := new(ipn.MemoryStore)
ln, _, err := safesocket.Listen(socketPath, 0)
if err != nil {
t.Fatal(err)
}
defer ln.Close()
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
t.Logf("ipnserver.Run = %v", err)
}

View File

@@ -70,9 +70,14 @@ func (s *Status) Peers() []key.NodePublic {
}
type PeerStatusLite struct {
// TxBytes/RxBytes is the total number of bytes transmitted to/received from this peer.
TxBytes, RxBytes int64
LastHandshake time.Time
NodeKey tailcfg.NodeKey
// LastHandshake is the last time a handshake succeeded with this peer.
// (Or we got key confirmation via the first data message,
// which is approximately the same thing.)
LastHandshake time.Time
// NodeKey is this peer's public node key.
NodeKey key.NodePublic
}
type PeerStatus struct {
@@ -83,22 +88,23 @@ type PeerStatus struct {
OS string // HostInfo.OS
UserID tailcfg.UserID
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
// Endpoints:
Addrs []string
CurAddr string // one of Addrs, or unique if roaming
Relay string // DERP region
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
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.
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
LastWrite time.Time // time last packet sent
LastSeen time.Time // last seen to tailcontrol; only present if offline
LastHandshake time.Time // with local wireguard
Online bool // whether node is connected to the control plane
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
@@ -237,9 +243,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if v := st.UserID; v != 0 {
e.UserID = v
}
if v := st.TailAddrDeprecated; v != "" {
e.TailAddrDeprecated = v
}
if v := st.TailscaleIPs; v != nil {
e.TailscaleIPs = v
}
@@ -270,6 +273,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if v := st.LastWrite; !v.IsZero() {
e.LastWrite = v
}
if st.Online {
e.Online = true
}
if st.InNetworkMap {
e.InNetworkMap = true
}
@@ -285,6 +291,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if st.ExitNode {
e.ExitNode = true
}
if st.ExitNodeOption {
e.ExitNodeOption = true
}
if st.ShareeNode {
e.ShareeNode = true
}

View File

@@ -12,7 +12,6 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -20,7 +19,6 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"inet.af/netaddr"
@@ -28,9 +26,9 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netknob"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/version"
)
@@ -113,6 +111,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveSetDNS(w, r)
case "/localapi/v0/derpmap":
h.serveDERPMap(w, r)
case "/localapi/v0/metrics":
h.serveMetrics(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
@@ -184,6 +184,17 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
w.Write(buf)
}
func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
// Require write access out of paranoia that the metrics
// might contain something sensitive.
if !h.PermitWrite {
http.Error(w, "metric access denied", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)
@@ -362,6 +373,25 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(fts)
}
// serveFilePut sends a file to another node.
//
// It's sometimes possible for clients to do this themselves, without
// tailscaled, except in the case of tailscaled running in
// userspace-networking ("netstack") mode, in which case tailscaled
// needs to a do a netstack dial out.
//
// Instead, the CLI also goes through tailscaled so it doesn't need to be
// aware of the network mode in use.
//
// macOS/iOS have always used this localapi method to simplify the GUI
// clients.
//
// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/)
// directly, as the Windows GUI always runs in tun mode anyway.
//
// URL format:
//
// * PUT /localapi/v0/file-put/:stableID/:escaped-filename
func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "file access denied", http.StatusForbidden)
@@ -409,7 +439,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
outReq.ContentLength = r.ContentLength
rp := httputil.NewSingleHostReverseProxy(dstURL)
rp.Transport = getDialPeerTransport(h.b)
rp.Transport = h.b.Dialer().PeerAPITransport()
rp.ServeHTTP(w, outReq)
}
@@ -443,26 +473,6 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
e.Encode(h.b.DERPMap())
}
var dialPeerTransportOnce struct {
sync.Once
v *http.Transport
}
func getDialPeerTransport(b *ipnlocal.LocalBackend) *http.Transport {
dialPeerTransportOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Dial = nil
dialer := net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
Control: b.PeerDialControlFunc(),
}
t.DialContext = dialer.DialContext
dialPeerTransportOnce.v = t
})
return dialPeerTransportOnce.v
}
func defBool(a string, def bool) bool {
if a == "" {
return def

View File

@@ -14,7 +14,8 @@ import (
// system (a version.OS value) is an interesting enough port to report
// to our peer nodes for discovery purposes.
func IsInterestingService(s tailcfg.Service, os string) bool {
if s.Proto == "peerapi4" || s.Proto == "peerapi6" {
switch s.Proto {
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
return true
}
if s.Proto != tailcfg.TCP {

View File

@@ -23,7 +23,7 @@ import (
)
const (
parameterNameRxStr = `^parameter/(.*)`
parameterNameRxStr = `^parameter(/.*)`
)
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)

View File

@@ -9,6 +9,7 @@ package filelogger
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
@@ -26,30 +27,30 @@ const (
maxFiles = 50
)
// New returns a logf wrapper that appends to local disk log
// New returns a Writer that appends to local disk log
// files on Windows, rotating old log files as needed to stay under
// file count & byte limits.
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
func New(fileBasePrefix, logID string, inner *log.Logger) io.Writer {
if runtime.GOOS != "windows" {
panic("not yet supported on any platform except Windows")
}
if logf == nil {
panic("nil logf")
if inner == nil {
panic("nil inner logger")
}
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return logf
inner.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return inner.Writer()
}
logf("local disk logdir: %v", dir)
inner.Printf("local disk logdir: %v", dir)
lfw := &logFileWriter{
fileBasePrefix: fileBasePrefix,
logID: logID,
dir: dir,
wrappedLogf: logf,
wrappedLogf: inner.Printf,
}
return lfw.Logf
return logger.FuncWriter(lfw.Logf)
}
// logFileWriter is the state for the log writer & rotator.
@@ -106,6 +107,7 @@ func (w *logFileWriter) appendToFileLocked(out []byte) {
if w.fday != day {
w.startNewFileLocked()
}
out = removeDatePrefix(out)
if w.f != nil {
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
const formatPre = "2006-01-02T15:04:05"
@@ -118,6 +120,30 @@ func (w *logFileWriter) appendToFileLocked(out []byte) {
}
}
func isNum(b byte) bool { return '0' <= b && b <= '9' }
// removeDatePrefix returns a subslice of v with the log package's
// standard datetime prefix format removed, if present.
func removeDatePrefix(v []byte) []byte {
const format = "2009/01/23 01:23:23 "
if len(v) < len(format) {
return v
}
for i, b := range v[:len(format)] {
fb := format[i]
if isNum(fb) {
if !isNum(b) {
return v
}
continue
}
if b != fb {
return v
}
}
return v[len(format):]
}
// startNewFileLocked opens a new log file for writing
// and also cleans up any old files.
//

View File

@@ -0,0 +1,28 @@
// 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 filelogger
import "testing"
func TestRemoveDatePrefix(t *testing.T) {
tests := []struct {
in, want string
}{
{"", ""},
{"\n", "\n"},
{"2009/01/23 01:23:23", "2009/01/23 01:23:23"},
{"2009/01/23 01:23:23 \n", "\n"},
{"2009/01/23 01:23:23 foo\n", "foo\n"},
{"9999/01/23 01:23:23 foo\n", "foo\n"},
{"2009_01/23 01:23:23 had an underscore\n", "2009_01/23 01:23:23 had an underscore\n"},
}
for i, tt := range tests {
got := removeDatePrefix([]byte(tt.in))
if string(got) != tt.want {
t.Logf("[%d] removeDatePrefix(%q) = %q; want %q", i, tt.in, got, tt.want)
}
}
}

View File

@@ -31,6 +31,8 @@ import (
"tailscale.com/atomicfile"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netknob"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
@@ -38,6 +40,7 @@ import (
"tailscale.com/paths"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/racebuild"
"tailscale.com/util/winutil"
"tailscale.com/version"
@@ -498,14 +501,17 @@ func New(collection string) *Policy {
}
return w
},
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost)},
}
if collection == logtail.CollectionNode {
c.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
}
if val := getLogTarget(); val != "" {
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
c.BaseURL = val
u, _ := url.Parse(val)
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
c.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host)}
}
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{
@@ -519,7 +525,7 @@ func New(collection string) *Policy {
}
lw := logtail.NewLogger(c, log.Printf)
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.SetOutput(maybeWrapForPlatform(lw, cmdName, newc.PublicID.String()))
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long,
@@ -565,9 +571,12 @@ func (p *Policy) Shutdown(ctx context.Context) error {
return nil
}
// newLogtailTransport returns the HTTP Transport we use for uploading
// logs to the given host name.
func newLogtailTransport(host string) *http.Transport {
// NewLogtailTransport returns an HTTP Transport particularly suited to uploading
// logs to the given host name. This includes:
// - If DNS lookup fails, consult the bootstrap DNS list of Tailscale hostnames.
// - If TLS connection fails, try again using LetsEncrypt's built-in root certificate,
// for the benefit of older OS platforms which might not include it.
func NewLogtailTransport(host string) *http.Transport {
// Start with a copy of http.DefaultTransport and tweak it a bit.
tr := http.DefaultTransport.(*http.Transport).Clone()
@@ -581,17 +590,29 @@ func newLogtailTransport(host string) *http.Transport {
// Log whenever we dial:
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
nd := netns.FromDialer(&net.Dialer{
nd := netns.FromDialer(log.Printf, &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
})
t0 := time.Now()
c, err := nd.DialContext(ctx, netw, addr)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
log.Printf("logtail: dial %q failed: %v (in %v)", addr, err, d)
} else {
if err == nil {
log.Printf("logtail: dialed %q in %v", addr, d)
return c, nil
}
// If we failed to dial, try again with bootstrap DNS.
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
}
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
c, err = dialer(ctx, netw, addr)
if err == nil {
log.Printf("logtail: bootstrap dial succeeded")
}
return c, err
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package logpolicy
import (
"io"
)
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
return lw
}

View File

@@ -0,0 +1,26 @@
// 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 logpolicy
import (
"io"
"log"
"golang.org/x/sys/windows/svc"
"tailscale.com/log/filelogger"
)
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
if cmdName != "tailscaled" {
return lw
}
isSvc, err := svc.IsWindowsService()
if err != nil || !isSvc {
return lw
}
return filelogger.New("tailscale-service", logID, log.New(lw, "", 0))
}

View File

@@ -28,6 +28,12 @@ import (
// Config.BaseURL isn't provided.
const DefaultHost = "log.tailscale.io"
const (
// CollectionNode is the name of a logtail Config.Collection
// for tailscaled (or equivalent: IPNExtension, Android app).
CollectionNode = "tailnode.log.tailscale.io"
)
type Encoder interface {
EncodeAll(src, dst []byte) []byte
Close() error
@@ -46,6 +52,12 @@ type Config struct {
Buffer Buffer // temp storage, if nil a MemoryBuffer
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
// MetricsDelta, if non-nil, is a func that returns an encoding
// delta in clientmetrics to upload alongside existing logs.
// It can return either an empty string (for nothing) or a string
// that's safe to embed in a JSON string literal without further escaping.
MetricsDelta func() string
// DrainLogs, if non-nil, disables automatic uploading of new logs,
// so that logs are only uploaded when a token is sent to DrainLogs.
DrainLogs <-chan struct{}
@@ -84,6 +96,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
drainLogs: cfg.DrainLogs,
timeNow: cfg.TimeNow,
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
metricsDelta: cfg.MetricsDelta,
shutdownStart: make(chan struct{}),
shutdownDone: make(chan struct{}),
@@ -119,6 +132,7 @@ type Logger struct {
zstdEncoder Encoder
uploadCancel func()
explainedRaw bool
metricsDelta func() string // or nil
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closed when shutdown complete
@@ -426,6 +440,14 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
b = append(b, "\"}, "...)
}
if l.metricsDelta != nil {
if d := l.metricsDelta(); d != "" {
b = append(b, `"metrics": "`...)
b = append(b, d...)
b = append(b, `",`...)
}
}
b = append(b, "\"text\": \""...)
for i, c := range buf {
switch c {

View File

@@ -7,6 +7,7 @@ package dns
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"fmt"
"io"
@@ -16,6 +17,7 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/types/logger"
@@ -50,10 +52,17 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
i := strings.IndexByte(line, '#')
if i >= 0 {
line = line[:i]
}
if strings.HasPrefix(line, "nameserver") {
nameserver := strings.TrimPrefix(line, "nameserver")
nameserver = strings.TrimSpace(nameserver)
s := strings.TrimPrefix(line, "nameserver")
nameserver := strings.TrimSpace(s)
if len(nameserver) == len(s) {
return OSConfig{}, fmt.Errorf("missing space after \"nameserver\" in %q", line)
}
ip, err := netaddr.ParseIP(nameserver)
if err != nil {
return OSConfig{}, err
@@ -63,8 +72,12 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
}
if strings.HasPrefix(line, "search") {
domain := strings.TrimPrefix(line, "search")
domain = strings.TrimSpace(domain)
s := strings.TrimPrefix(line, "search")
domain := strings.TrimSpace(s)
if len(domain) == len(s) {
// No leading space?!
return OSConfig{}, fmt.Errorf("missing space after \"domain\" in %q", line)
}
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
@@ -121,12 +134,20 @@ func isResolvedRunning() bool {
return false
}
// is-active exits with code 3 if the service is not active.
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
// is-active exits with code 3 if the service is not active.
return err == nil
}
func restartResolved() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
}
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
// generated from the given configuration, creating a backup of its old state.
//
@@ -383,7 +404,12 @@ func (m *directManager) Close() error {
}
if isResolvedRunning() && !runningAsGUIDesktopUser() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
m.logf("restarting systemd-resolved...")
if err := restartResolved(); err != nil {
m.logf("restart of systemd-resolved failed: %v", err)
} else {
m.logf("restarted systemd-resolved")
}
}
return nil

View File

@@ -14,6 +14,7 @@ import (
"syscall"
"testing"
qt "github.com/frankban/quicktest"
"inet.af/netaddr"
"tailscale.com/util/dnsname"
)
@@ -138,3 +139,61 @@ func TestDirectBrokenRemove(t *testing.T) {
}
testDirect(t, brokenRemoveFS{directFS{prefix: tmp}})
}
func TestReadResolve(t *testing.T) {
c := qt.New(t)
tests := []struct {
in string
want OSConfig
wantErr bool
}{
{in: `nameserver 192.168.0.100`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver 192.168.0.100 # comment`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver 192.168.0.100#`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver #192.168.0.100`, wantErr: true},
{in: `nameserver`, wantErr: true},
{in: `# nameserver 192.168.0.100`, want: OSConfig{}},
{in: `nameserver192.168.0.100`, wantErr: true},
{in: `search tailsacle.com`,
want: OSConfig{
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
},
},
{in: `search tailsacle.com # typo`,
want: OSConfig{
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
},
},
{in: `searchtailsacle.com`, wantErr: true},
{in: `search`, wantErr: true},
}
for _, test := range tests {
cfg, err := readResolv(strings.NewReader(test.in))
if test.wantErr {
c.Assert(err, qt.IsNotNil)
} else {
c.Assert(err, qt.IsNil)
}
c.Assert(cfg, qt.DeepEquals, test.want)
}
}

View File

@@ -26,8 +26,8 @@ func TestParseIni(t *testing.T) {
[network] # trailing comment
generateResolvConf = false # trailing comment`,
want: map[string]map[string]string{
"automount": map[string]string{"enabled": "true", "root": "/mnt/"},
"network": map[string]string{"generateResolvConf": "false"},
"automount": {"enabled": "true", "root": "/mnt/"},
"network": {"generateResolvConf": "false"},
},
},
}

View File

@@ -10,8 +10,10 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
@@ -35,22 +37,26 @@ type Manager struct {
resolver *resolver.Resolver
os OSConfigurator
config Config
}
// NewManagers created a new manager from the given config.
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, linkSel resolver.ForwardLinkSelector) *Manager {
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
if dialer == nil {
panic("nil Dialer")
}
logf = logger.WithPrefix(logf, "dns: ")
m := &Manager{
logf: logf,
resolver: resolver.New(logf, linkMon, linkSel),
resolver: resolver.New(logf, linkMon, linkSel, dialer),
os: oscfg,
}
m.logf("using %T", m.os)
return m
}
// Resolver returns the Manager's DNS Resolver.
func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
func (m *Manager) Set(cfg Config) error {
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
cfg.WriteToBufioWriter(w)
@@ -70,8 +76,10 @@ func (m *Manager) Set(cfg Config) error {
return err
}
if err := m.os.SetDNS(ocfg); err != nil {
health.SetDNSOSHealth(err)
return err
}
health.SetDNSOSHealth(nil)
return nil
}
@@ -161,6 +169,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
if !m.os.SupportsSplitDNS() || isWindows {
bcfg, err := m.os.GetBaseConfig()
if err != nil {
health.SetDNSOSHealth(err)
return resolver.Config{}, OSConfig{}, err
}
var defaultRoutes []dnstype.Resolver
@@ -225,7 +234,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
logf("creating dns cleanup: %v", err)
return
}
dns := NewManager(logf, oscfg, nil, nil)
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -180,20 +180,55 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
return "direct", nil
}
case "NetworkManager":
// You'd think we would use newNMManager somewhere in
// here. However, as explained in
// https://github.com/tailscale/tailscale/issues/1699 , using
// NetworkManager for DNS configuration carries with it the
// cost of losing IPv6 configuration on the Tailscale network
// interface. So, when we can avoid it, we bypass
// NetworkManager by replacing resolv.conf directly.
//
// If you ever try to put NMManager back here, keep in mind
// that versions >=1.26.6 will ignore DNS configuration
// anyway, so you still need a fallback path that uses
// directManager.
dbg("rc", "nm")
return "direct", nil
// Sometimes, NetworkManager owns the configuration but points
// it at systemd-resolved.
if err := resolvedIsActuallyResolver(bs); err != nil {
dbg("resolved", "not-in-use")
// You'd think we would use newNMManager here. However, as
// explained in
// https://github.com/tailscale/tailscale/issues/1699 ,
// using NetworkManager for DNS configuration carries with
// it the cost of losing IPv6 configuration on the
// Tailscale network interface. So, when we can avoid it,
// we bypass NetworkManager by replacing resolv.conf
// directly.
//
// If you ever try to put NMManager back here, keep in mind
// that versions >=1.26.6 will ignore DNS configuration
// anyway, so you still need a fallback path that uses
// directManager.
return "direct", nil
}
dbg("nm-resolved", "yes")
if err := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1"); err != nil {
dbg("resolved", "no")
return "direct", nil
}
// See large comment above for reasons we'd use NM rather than
// resolved. systemd-resolved is actually in charge of DNS
// configuration, but in some cases we might need to configure
// it via NetworkManager. All the logic below is probing for
// that case: is NetworkManager running? If so, is it one of
// the versions that requires direct interaction with it?
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
dbg("nm", "no")
return "systemd-resolved", nil
}
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
if err != nil {
// Failed to figure out NM's version, can't make a correct
// decision.
return "", fmt.Errorf("checking NetworkManager version: %v", err)
}
if safe {
dbg("nm-safe", "yes")
return "network-manager", nil
}
dbg("nm-safe", "no")
return "systemd-resolved", nil
default:
dbg("rc", "unknown")
return "direct", nil
@@ -244,6 +279,13 @@ func nmIsUsingResolved() error {
return nil
}
// resolvedIsActuallyResolver reports whether the given resolv.conf
// bytes describe a configuration where systemd-resolved (127.0.0.53)
// is the only configured nameserver.
//
// Returns an error if the configuration is something other than
// exclusively systemd-resolved, or nil if the config is only
// systemd-resolved.
func resolvedIsActuallyResolver(bs []byte) error {
cfg, err := readResolv(bytes.NewBuffer(bs))
if err != nil {

View File

@@ -34,7 +34,7 @@ func TestLinuxDNSMode(t *testing.T) {
resolvDotConf(
"# Managed by NetworkManager",
"nameserver 10.0.0.1")),
wantLog: "dns: [rc=nm ret=direct]",
wantLog: "dns: [rc=nm resolved=not-in-use ret=direct]",
want: "direct",
},
{
@@ -172,6 +172,52 @@ func TestLinuxDNSMode(t *testing.T) {
wantLog: "dns: [rc=resolved resolved=no ret=direct]",
want: "direct",
},
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name: "networkmanager_but_pointing_at_systemd-resolved",
env: env(resolvDotConf(
"# Generated by NetworkManager",
"nameserver 127.0.0.53",
"options edns0 trust-ad"),
resolvedRunning(),
nmRunning("1.32.12", true)),
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=no ret=systemd-resolved]",
want: "systemd-resolved",
},
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name: "networkmanager_but_pointing_at_systemd-resolved_but_no_resolved",
env: env(resolvDotConf(
"# Generated by NetworkManager",
"nameserver 127.0.0.53",
"options edns0 trust-ad"),
nmRunning("1.32.12", true)),
wantLog: "dns: [rc=nm nm-resolved=yes resolved=no ret=direct]",
want: "direct",
},
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name: "networkmanager_but_pointing_at_systemd-resolved_and_safe_nm",
env: env(resolvDotConf(
"# Generated by NetworkManager",
"nameserver 127.0.0.53",
"options edns0 trust-ad"),
resolvedRunning(),
nmRunning("1.26.3", true)),
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
want: "network-manager",
},
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name: "networkmanager_but_pointing_at_systemd-resolved_and_no_networkmanager",
env: env(resolvDotConf(
"# Generated by NetworkManager",
"nameserver 127.0.0.53",
"options edns0 trust-ad"),
resolvedRunning()),
wantLog: "dns: [rc=nm nm-resolved=yes nm=no ret=systemd-resolved]",
want: "systemd-resolved",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"inet.af/netaddr"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@@ -398,7 +399,7 @@ func TestManager(t *testing.T) {
SplitDNS: test.split,
BaseConfig: test.bs,
}
m := NewManager(t.Logf, &f, nil, nil)
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil)
m.resolver.TestOnlySetHook(f.SetResolver)
if err := m.Set(test.in); err != nil {

View File

@@ -12,10 +12,13 @@ import (
"errors"
"fmt"
"net"
"strings"
"sync"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -39,6 +42,28 @@ var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
var errNotReady = errors.New("interface not ready")
// DBus entities we talk to.
//
// DBus is an RPC bus. In particular, the bus we're talking to is the
// system-wide bus (there is also a per-user session bus for
// user-specific applications).
//
// Daemons connect to the bus, and advertise themselves under a
// well-known object name. That object exposes paths, and each path
// implements one or more interfaces that contain methods, properties,
// and signals.
//
// Clients connect to the bus and walk that same hierarchy to invoke
// RPCs, get/set properties, or listen for signals.
const (
dbusResolvedObject = "org.freedesktop.resolve1"
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
dbusInterface = "org.freedesktop.DBus"
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
)
type resolvedLinkNameserver struct {
Family int32
Address []byte
@@ -83,9 +108,15 @@ func isResolvedActive() bool {
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
type resolvedManager struct {
logf logger.Logf
ifidx int
resolved dbus.BusObject
logf logger.Logf
ifidx int
cancelSyncer context.CancelFunc // run to shut down syncer goroutine
syncerDone chan struct{} // closed when syncer is stopped
resolved dbus.BusObject
mu sync.Mutex // guards RPCs made by syncLocked, and the following
config OSConfig // last SetDNS config
}
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
@@ -99,19 +130,88 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
return nil, err
}
return &resolvedManager{
logf: logf,
ifidx: iface.Index,
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
}, nil
ctx, cancel := context.WithCancel(context.Background())
ret := &resolvedManager{
logf: logf,
ifidx: iface.Index,
cancelSyncer: cancel,
syncerDone: make(chan struct{}),
resolved: conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath)),
}
signals := make(chan *dbus.Signal, 16)
go ret.resync(ctx, signals)
// Only receive the DBus signals we need to resync our config on
// resolved restart. Failure to set filters isn't a fatal error,
// we'll just receive all broadcast signals and have to ignore
// them on our end.
if err := conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
logf("[v1] Setting DBus signal filter failed: %v", err)
}
conn.Signal(signals)
return ret, nil
}
func (m *resolvedManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
m.mu.Lock()
defer m.mu.Unlock()
m.config = config
return m.syncLocked(context.TODO()) // would be nice to plumb context through from SetDNS
}
func (m *resolvedManager) resync(ctx context.Context, signals chan *dbus.Signal) {
defer close(m.syncerDone)
for {
select {
case <-ctx.Done():
return
case signal := <-signals:
// In theory the signal was filtered by DBus, but if
// AddMatchSignal in the constructor failed, we may be
// getting other spam.
if signal.Path != dbusPath || signal.Name != dbusInterface+"."+dbusOwnerSignal {
continue
}
// signal.Body is a []interface{} of 3 strings: bus name, previous owner, new owner.
if len(signal.Body) != 3 {
m.logf("[unexpectected] DBus NameOwnerChanged len(Body) = %d, want 3")
}
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
continue
}
newOwner, ok := signal.Body[2].(string)
if !ok {
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
}
if newOwner == "" {
// systemd-resolved left the bus, no current owner,
// nothing to do.
continue
}
// The resolved bus name has a new owner, meaning resolved
// restarted. Reprogram current config.
m.logf("systemd-resolved restarted, syncing DNS config")
m.mu.Lock()
err := m.syncLocked(ctx)
// Set health while holding the lock, because this will
// graciously serialize the resync's health outcome with a
// concurrent SetDNS call.
health.SetDNSOSHealth(err)
m.mu.Unlock()
if err != nil {
m.logf("failed to configure systemd-resolved: %v", err)
}
}
}
}
func (m *resolvedManager) syncLocked(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
defer cancel()
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
for i, server := range config.Nameservers {
var linkNameservers = make([]resolvedLinkNameserver, len(m.config.Nameservers))
for i, server := range m.config.Nameservers {
ip := server.As16()
if server.Is4() {
linkNameservers[i] = resolvedLinkNameserver{
@@ -127,16 +227,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
}
err := m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
m.ifidx, linkNameservers,
).Store()
if err != nil {
return fmt.Errorf("setLinkDNS: %w", err)
}
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
linkDomains := make([]resolvedLinkDomain, 0, len(m.config.SearchDomains)+len(m.config.MatchDomains))
seenDomains := map[dnsname.FQDN]bool{}
for _, domain := range config.SearchDomains {
for _, domain := range m.config.SearchDomains {
if seenDomains[domain] {
continue
}
@@ -146,7 +246,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: false,
})
}
for _, domain := range config.MatchDomains {
for _, domain := range m.config.MatchDomains {
if seenDomains[domain] {
// Search domains act as both search and match in
// resolved, so it's correct to skip.
@@ -158,7 +258,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: true,
})
}
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
if len(m.config.MatchDomains) == 0 && len(m.config.Nameservers) > 0 {
// Caller requested full DNS interception, install a
// routing-only root domain.
linkDomains = append(linkDomains, resolvedLinkDomain{
@@ -168,14 +268,22 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
}
err = m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomains,
).Store()
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
// Issue 3188: older systemd-resolved had argument length limits.
// Trim out the *.arpa. entries and try again.
err = m.resolved.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
).Store()
}
if err != nil {
return fmt.Errorf("setLinkDomains: %w", err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(m.config.MatchDomains) == 0); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
// but otherwise it's working good
@@ -190,26 +298,26 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
// or something).
// Disable LLMNR, we don't do multicast.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
}
// Disable mdns.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable mdns: %v", call.Err)
}
// We don't support dnssec consistently right now, force it off to
// avoid partial failures when we split DNS internally.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DoT: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.FlushCaches", 0); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
m.logf("failed to flush resolved DNS cache: %v", call.Err)
}
@@ -225,12 +333,32 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
}
func (m *resolvedManager) Close() error {
m.cancelSyncer()
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
return fmt.Errorf("RevertLink: %w", call.Err)
}
select {
case <-m.syncerDone:
case <-ctx.Done():
m.logf("timeout in systemd-resolved syncer shutdown")
}
return nil
}
// linkDomainsWithoutReverseDNS returns a copy of v without
// *.arpa. entries.
func linkDomainsWithoutReverseDNS(v []resolvedLinkDomain) (ret []resolvedLinkDomain) {
for _, d := range v {
if strings.HasSuffix(d.Domain, ".arpa.") {
// Oh well. At least the rest will work.
continue
}
ret = append(ret, d)
}
return ret
}

View File

@@ -26,6 +26,7 @@ import (
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
@@ -159,7 +160,8 @@ type resolverAndDelay struct {
type forwarder struct {
logf logger.Logf
linkMon *monitor.Mon
linkSel ForwardLinkSelector
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
dialer *tsdial.Dialer
dohSem chan struct{}
ctx context.Context // good until Close
@@ -205,11 +207,12 @@ func maxDoHInFlight(goos string) int {
return 1000
}
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
f := &forwarder{
logf: logger.WithPrefix(logf, "forward: "),
linkMon: linkMon,
linkSel: linkSel,
dialer: dialer,
responses: responses,
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
}
@@ -342,7 +345,7 @@ func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Cl
if f.dohClient == nil {
f.dohClient = map[string]*http.Client{}
}
nsDialer := netns.NewDialer()
nsDialer := netns.NewDialer(f.logf)
c = &http.Client{
Transport: &http.Transport{
IdleConnTimeout: dohTransportTimeout,
@@ -385,6 +388,7 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
}
defer f.releaseDoHSem()
metricDNSFwdDoH.Add(1)
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
if err != nil {
return nil, err
@@ -398,16 +402,23 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
hres, err := c.Do(req)
if err != nil {
metricDNSFwdDoHErrorTransport.Add(1)
return nil, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
metricDNSFwdDoHErrorStatus.Add(1)
return nil, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
return ioutil.ReadAll(hres.Body)
res, err := ioutil.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
return res, err
}
// send sends packet to dst. It is best effort.
@@ -415,12 +426,14 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
if strings.HasPrefix(rr.name.Addr, "http://") {
return nil, fmt.Errorf("http:// resolvers not supported yet")
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
}
if strings.HasPrefix(rr.name.Addr, "https://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("https:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "tls://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("tls:// resolvers not supported yet")
}
ipp, err := netaddr.ParseIPPort(rr.name.Addr)
@@ -438,6 +451,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
f.logf("DoH error from %v: %v", ipp.IP(), err)
}
metricDNSFwdUDP.Add(1)
ln, err := f.packetListener(ipp.IP())
if err != nil {
return nil, err
@@ -453,11 +467,13 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
defer fq.closeOnCtxDone.Remove(conn)
if _, err := conn.WriteTo(fq.packet, ipp.UDPAddr()); err != nil {
metricDNSFwdUDPErrorWrite.Add(1)
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, err
}
metricDNSFwdUDPWrote.Add(1)
// The 1 extra byte is to detect packet truncation.
out := make([]byte, maxResponseBytes+1)
@@ -469,6 +485,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
if packetWasTruncated(err) {
err = nil
} else {
metricDNSFwdUDPErrorRead.Add(1)
return nil, err
}
}
@@ -482,12 +499,14 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
out = out[:n]
txid := getTxID(out)
if txid != fq.txid {
metricDNSFwdUDPErrorTxID.Add(1)
return nil, errors.New("txid doesn't match")
}
rcode := getRCode(out)
// don't forward transient errors back to the client when the server fails
if rcode == dns.RCodeServerFailure {
f.logf("recv: response code indicating server failure: %d", rcode)
metricDNSFwdUDPErrorServer.Add(1)
return nil, errors.New("response code indicates server issue")
}
@@ -505,7 +524,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
}
clampEDNSSize(out, maxResponseBytes)
metricDNSFwdUDPSuccess.Add(1)
return out, nil
}
@@ -546,10 +565,30 @@ type forwardQuery struct {
// ...
}
// forward forwards the query to all upstream nameservers and returns the first response.
// forward forwards the query to all upstream nameservers and waits for
// the first response.
//
// It either sends to f.responses and returns nil, or returns a
// non-nil error (without sending to the channel).
func (f *forwarder) forward(query packet) error {
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
defer cancel()
return f.forwardWithDestChan(ctx, query, f.responses)
}
// forward forwards the query to all upstream nameservers and waits
// for the first response.
//
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
//
// If resolvers is non-empty, it's used explicitly (notably, for exit
// node DNS proxy queries), otherwise f.resolvers is used.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, resolvers ...resolverAndDelay) error {
metricDNSFwd.Add(1)
domain, err := nameFromQuery(query.bs)
if err != nil {
metricDNSFwdErrorName.Add(1)
return err
}
@@ -558,14 +597,18 @@ func (f *forwarder) forward(query packet) error {
// when browsing for LAN devices. But even when filtering this
// out, playing on Sonos still works.
if hasRDNSBonjourPrefix(domain) {
metricDNSFwdDropBonjour.Add(1)
return nil
}
clampEDNSSize(query.bs, maxResponseBytes)
resolvers := f.resolvers(domain)
if len(resolvers) == 0 {
return errNoUpstreams
resolvers = f.resolvers(domain)
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
}
}
fq := &forwardQuery{
@@ -575,9 +618,6 @@ func (f *forwarder) forward(query packet) error {
}
defer fq.closeOnCtxDone.Close()
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
defer cancel()
resc := make(chan []byte, 1)
var (
mu sync.Mutex
@@ -615,14 +655,18 @@ func (f *forwarder) forward(query packet) error {
case v := <-resc:
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return ctx.Err()
case f.responses <- packet{v, query.addr}:
case responseChan <- packet{v, query.addr}:
metricDNSFwdSuccess.Add(1)
return nil
}
case <-ctx.Done():
mu.Lock()
defer mu.Unlock()
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
return ctx.Err()

View File

@@ -8,10 +8,14 @@ package resolver
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"sort"
"strings"
@@ -19,11 +23,16 @@ import (
"sync/atomic"
"time"
"go4.org/mem"
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/wgengine/monitor"
)
@@ -184,6 +193,7 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]dnstype.Resolver) {
type Resolver struct {
logf logger.Logf
linkMon *monitor.Mon // or nil
dialer *tsdial.Dialer // non-nil
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
// forwarder forwards requests to upstream nameservers.
forwarder *forwarder
@@ -215,7 +225,10 @@ type ForwardLinkSelector interface {
// New returns a new resolver.
// linkMon optionally specifies a link monitor to use for socket rebinding.
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *Resolver {
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *Resolver {
if dialer == nil {
panic("nil Dialer")
}
r := &Resolver{
logf: logger.WithPrefix(logf, "resolver: "),
linkMon: linkMon,
@@ -224,8 +237,9 @@ func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *R
closed: make(chan struct{}),
hostToIP: map[dnsname.FQDN][]netaddr.IP{},
ipToHost: map[netaddr.IP]dnsname.FQDN{},
dialer: dialer,
}
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel)
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel, dialer)
return r
}
@@ -272,13 +286,16 @@ func (r *Resolver) Close() {
// It takes ownership of the payload and does not block.
// If the queue is full, the request will be dropped and an error will be returned.
func (r *Resolver) EnqueueRequest(bs []byte, from netaddr.IPPort) error {
metricDNSQueryLocal.Add(1)
select {
case <-r.closed:
metricDNSQueryErrorClosed.Add(1)
return ErrClosed
default:
}
if n := atomic.AddInt32(&r.activeQueriesAtomic, 1); n > maxActiveQueries() {
atomic.AddInt32(&r.activeQueriesAtomic, -1)
metricDNSQueryErrorQueue.Add(1)
return errFullQueue
}
go r.handleQuery(packet{bs, from})
@@ -298,14 +315,259 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
}
}
// parseExitNodeQuery parses a DNS request packet.
// It returns nil if it's malformed or lacking a question.
func parseExitNodeQuery(q []byte) *response {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
p.zeroParser()
defer p.zeroParser()
if err := p.parseQuery(q); err != nil {
return nil
}
return p.response()
}
// HandleExitNodeDNSQuery handles a DNS query that arrived from a peer
// via the peerapi's DoH server. This is only used when the local
// node is being an exit node.
//
// The provided allowName callback is whether a DNS query for a name
// (as found by parsing q) is allowed.
//
// In most (all?) cases, err will be nil. A bogus DNS query q will
// still result in a response DNS packet (saying there's a failure)
// and a nil error.
// TODO: figure out if we even need an error result.
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort, allowName func(name string) bool) (res []byte, err error) {
metricDNSExitProxyQuery.Add(1)
ch := make(chan packet, 1)
resp := parseExitNodeQuery(q)
if resp == nil {
return nil, errors.New("bad query")
}
name := resp.Question.Name.String()
if !allowName(name) {
metricDNSExitProxyErrorName.Add(1)
resp.Header.RCode = dns.RCodeRefused
return marshalResponse(resp)
}
switch runtime.GOOS {
default:
return nil, errors.New("unsupported exit node OS")
case "windows":
// TODO: use DnsQueryEx and write to ch.
// See https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsqueryex.
// For now just use the net package:
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
case "darwin":
// /etc/resolv.conf is a lie and only says one upstream DNS
// but for now that's probably good enough. Later we'll
// want to blend in everything from scutil --dns.
fallthrough
case "linux", "freebsd", "openbsd", "illumos":
nameserver, err := stubResolverForOS()
if err != nil {
r.logf("stubResolverForOS: %v", err)
metricDNSExitProxyErrorResolvConf.Add(1)
return nil, err
}
// TODO: more than 1 resolver from /etc/resolv.conf?
var resolvers []resolverAndDelay
if nameserver == tsaddr.TailscaleServiceIP() {
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
// so avoid the loop through the kernel and just do what we
// would've done anyway. By not passing any resolvers, the forwarder
// will use its default ones from our DNS config.
} else {
resolvers = []resolverAndDelay{{
name: dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
}}
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolvers...)
if err != nil {
metricDNSExitProxyErrorForward.Add(1)
return nil, err
}
}
select {
case p, ok := <-ch:
if ok {
return p.bs, nil
}
panic("unexpected close chan")
default:
panic("unexpected unreadable chan")
}
}
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
// return a reply (for the ExitDNS DoH service) using the net package's
// native APIs. This is only used on Windows for now.
//
// If resolver is nil, the net.Resolver zero value is used.
//
// response contains the pre-serialized response, which notably
// includes the original question and its header.
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, resolver *net.Resolver, resp *response) (res []byte, err error) {
if resp.Question.Class != dns.ClassINET {
return nil, errors.New("unsupported class")
}
r := resolver
if r == nil {
r = new(net.Resolver)
}
name := resp.Question.Name.String()
handleError := func(err error) (res []byte, _ error) {
if isGoNoSuchHostError(err) {
resp.Header.RCode = dns.RCodeNameError
return marshalResponse(resp)
}
// TODO: map other errors to RCodeServerFailure?
// Or I guess our caller should do that?
return nil, err
}
resp.Header.RCode = dns.RCodeSuccess // unless changed below
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA:
network := "ip4"
if resp.Question.Type == dns.TypeAAAA {
network = "ip6"
}
ips, err := r.LookupIP(ctx, network, name)
if err != nil {
return handleError(err)
}
for _, stdIP := range ips {
if ip, ok := netaddr.FromStdIP(stdIP); ok {
resp.IPs = append(resp.IPs, ip)
}
}
case dns.TypeTXT:
strs, err := r.LookupTXT(ctx, name)
if err != nil {
return handleError(err)
}
resp.TXT = strs
case dns.TypePTR:
ipStr, ok := unARPA(name)
if !ok {
// TODO: is this RCodeFormatError?
return nil, errors.New("bogus PTR name")
}
addrs, err := r.LookupAddr(ctx, ipStr)
if err != nil {
return handleError(err)
}
if len(addrs) > 0 {
resp.Name, _ = dnsname.ToFQDN(addrs[0])
}
case dns.TypeCNAME:
cname, err := r.LookupCNAME(ctx, name)
if err != nil {
return handleError(err)
}
resp.CNAME = cname
case dns.TypeSRV:
// Thanks, Go: "To accommodate services publishing SRV
// records under non-standard names, if both service
// and proto are empty strings, LookupSRV looks up
// name directly."
_, srvs, err := r.LookupSRV(ctx, "", "", name)
if err != nil {
return handleError(err)
}
resp.SRVs = srvs
case dns.TypeNS:
nss, err := r.LookupNS(ctx, name)
if err != nil {
return handleError(err)
}
resp.NSs = nss
default:
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
}
return marshalResponse(resp)
}
func isGoNoSuchHostError(err error) bool {
if de, ok := err.(*net.DNSError); ok {
return de.IsNotFound
}
return false
}
type resolvConfCache struct {
mod time.Time
size int64
ip netaddr.IP
// TODO: inode/dev?
}
// resolvConfCacheValue contains the most recent stat metadata and parsed
// version of /etc/resolv.conf.
var resolvConfCacheValue atomic.Value // of resolvConfCache
var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
// stubResolverForOS returns the IP address of the first nameserver in
// /etc/resolv.conf.
func stubResolverForOS() (ip netaddr.IP, err error) {
fi, err := os.Stat("/etc/resolv.conf")
if err != nil {
return netaddr.IP{}, err
}
cur := resolvConfCache{
mod: fi.ModTime(),
size: fi.Size(),
}
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
return c.ip, nil
}
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
if !ip.IsZero() {
return nil
}
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
return nil
}
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
ip, err = netaddr.ParseIP(s)
return err
}
return nil
})
if err != nil {
return netaddr.IP{}, err
}
if !ip.IsValid() {
return netaddr.IP{}, errEmptyResolvConf
}
cur.ip = ip
resolvConfCacheValue.Store(cur)
return ip, nil
}
// resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL).
// Returns dns.RCodeRefused to indicate that the local map is not
// authoritative for domain.
func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, dns.RCode) {
metricDNSResolveLocal.Add(1)
// Reject .onion domains per RFC 7686.
if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
metricDNSResolveLocalErrorOnion.Add(1)
return netaddr.IP{}, dns.RCodeNameError
}
@@ -319,6 +581,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
for _, suffix := range localDomains {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.
metricDNSResolveLocalErrorMissing.Add(1)
return netaddr.IP{}, dns.RCodeNameError
}
}
@@ -336,30 +599,37 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
case dns.TypeA:
for _, ip := range addrs {
if ip.Is4() {
metricDNSResolveLocalOKA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoA.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
case dns.TypeAAAA:
for _, ip := range addrs {
if ip.Is6() {
metricDNSResolveLocalOKAAAA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoAAAA.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
case dns.TypeALL:
// Answer with whatever we've got.
// It could be IPv4, IPv6, or a zero addr.
// TODO: Return all available resolutions (A and AAAA, if we have them).
if len(addrs) == 0 {
metricDNSResolveLocalNoAll.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
}
metricDNSResolveLocalOKAll.Add(1)
return addrs[0], dns.RCodeSuccess
// Leave some some record types explicitly unimplemented.
// These types relate to recursive resolution or special
// DNS semantics and might be implemented in the future.
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
metricDNSResolveNotImplType.Add(1)
return netaddr.IP{}, dns.RCodeNotImplemented
// For everything except for the few types above that are explicitly not implemented, return no records.
@@ -369,6 +639,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
// dig -t TYPE9824 example.com
// and note that NOERROR is returned, despite that record type being made up.
default:
metricDNSResolveNoRecordType.Add(1)
// The name exists, but no records exist of the requested type.
return netaddr.IP{}, dns.RCodeSuccess
}
@@ -434,10 +705,27 @@ func (r *Resolver) handleQuery(pkt packet) {
type response struct {
Header dns.Header
Question dns.Question
// Name is the response to a PTR query.
Name dnsname.FQDN
// IP is the response to an A, AAAA, or ALL query.
IP netaddr.IP
// IP and IPs are the responses to an A, AAAA, or ALL query.
// Either/both/neither can be populated.
IP netaddr.IP
IPs []netaddr.IP
// TXT is the response to a TXT query.
// Each one is its own RR with one string.
TXT []string
// CNAME is the response to a CNAME query.
CNAME string
// SRVs are the responses to a SRV query.
SRVs []*net.SRV
// NSs are the responses to an NS query.
NSs []*net.NS
}
var dnsParserPool = &sync.Pool{
@@ -468,6 +756,7 @@ func (p *dnsParser) zeroParser() { p.parser = dns.Parser{} }
// p.Question.
func (p *dnsParser) parseQuery(query []byte) error {
defer p.zeroParser()
p.zeroParser()
var err error
p.Header, err = p.parser.Start(query)
if err != nil {
@@ -512,6 +801,16 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
return builder.AAAAResource(answerHeader, answer)
}
func marshalIP(name dns.Name, ip netaddr.IP, builder *dns.Builder) error {
if ip.Is4() {
return marshalARecord(name, ip, builder)
}
if ip.Is6() {
return marshalAAAARecord(name, ip, builder)
}
return nil
}
// marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
@@ -531,6 +830,83 @@ func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builde
return builder.PTRResource(answerHeader, answer)
}
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
for _, txt := range txts {
if err := builder.TXTResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeTXT,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.TXTResource{
TXT: []string{txt},
}); err != nil {
return err
}
}
return nil
}
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
if cname == "" {
return nil
}
name, err := dns.NewName(cname)
if err != nil {
return err
}
return builder.CNAMEResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeCNAME,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.CNAMEResource{
CNAME: name,
})
}
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
for _, ns := range nss {
name, err := dns.NewName(ns.Host)
if err != nil {
return err
}
err = builder.NSResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeNS,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.NSResource{NS: name})
if err != nil {
return err
}
}
return nil
}
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
for _, s := range srvs {
srvName, err := dns.NewName(s.Target)
if err != nil {
return err
}
err = builder.SRVResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeSRV,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.SRVResource{
Target: srvName,
Priority: s.Priority,
Port: s.Port,
Weight: s.Weight,
})
if err != nil {
return err
}
}
return nil
}
// marshalResponse serializes the DNS response into a new buffer.
func marshalResponse(resp *response) ([]byte, error) {
resp.Header.Response = true
@@ -541,6 +917,14 @@ func marshalResponse(resp *response) ([]byte, error) {
builder := dns.NewBuilder(nil, resp.Header)
// TODO(bradfitz): I'm not sure why this wasn't enabled
// before, but for now (2021-12-09) enable it at least when
// there's more than 1 record (which was never the case
// before), where it really helps.
if len(resp.IPs) > 1 {
builder.EnableCompression()
}
isSuccess := resp.Header.RCode == dns.RCodeSuccess
if resp.Question.Type != 0 || isSuccess {
@@ -567,13 +951,24 @@ func marshalResponse(resp *response) ([]byte, error) {
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
if resp.IP.Is4() {
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
} else if resp.IP.Is6() {
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
return nil, err
}
for _, ip := range resp.IPs {
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
return nil, err
}
}
case dns.TypePTR:
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
case dns.TypeTXT:
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
case dns.TypeCNAME:
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
case dns.TypeSRV:
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
case dns.TypeNS:
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
}
if err != nil {
return nil, err
@@ -700,6 +1095,7 @@ func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *respons
return nil, errNotOurName
}
metricDNSMagicDNSSuccessReverse.Add(1)
return marshalResponse(resp)
}
@@ -716,8 +1112,10 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
// We will not return this error: it is the sender's fault.
if err != nil {
if errors.Is(err, dns.ErrSectionDone) {
metricDNSErrorParseNoQ.Add(1)
r.logf("parseQuery(%02x): no DNS questions", query)
} else {
metricDNSErrorParseQuery.Add(1)
r.logf("parseQuery(%02x): %v", query, err)
}
resp := parser.response()
@@ -727,6 +1125,7 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
rawName := parser.Question.Name.Data[:parser.Question.Name.Length]
name, err := dnsname.ToFQDN(rawNameToLower(rawName))
if err != nil {
metricDNSErrorNotFQDN.Add(1)
// DNS packet unexpectedly contains an invalid FQDN.
resp := parser.response()
resp.Header.RCode = dns.RCodeFormatError
@@ -750,3 +1149,90 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
resp.IP = ip
return marshalResponse(resp)
}
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
func unARPA(a string) (ipStr string, ok bool) {
const suf4 = ".in-addr.arpa."
if strings.HasSuffix(a, suf4) {
s := strings.TrimSuffix(a, suf4)
// Parse and reverse octets.
ip, err := netaddr.ParseIP(s)
if err != nil || !ip.Is4() {
return "", false
}
a4 := ip.As4()
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
}
const suf6 = ".ip6.arpa."
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
strings.HasSuffix(a, suf6) {
var hx [32]byte
var a16 [16]byte
for i := range hx {
hx[31-i] = a[i*2]
if a[i*2+1] != '.' {
return "", false
}
}
hex.Decode(a16[:], hx[:])
return netaddr.IPFrom16(a16).String(), true
}
return "", false
}
var (
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
metricDNSErrorParseNoQ = clientmetric.NewCounter("dns_query_respond_error_no_question")
metricDNSErrorParseQuery = clientmetric.NewCounter("dns_query_respond_error_parse")
metricDNSErrorNotFQDN = clientmetric.NewCounter("dns_query_respond_error_not_fqdn")
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
metricDNSExitProxyQuery = clientmetric.NewCounter("dns_exit_node_query")
metricDNSExitProxyErrorName = clientmetric.NewCounter("dns_exit_node_error_name")
metricDNSExitProxyErrorForward = clientmetric.NewCounter("dns_exit_node_error_forward")
metricDNSExitProxyErrorResolvConf = clientmetric.NewCounter("dns_exit_node_error_resolvconf")
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")
metricDNSFwdErrorName = clientmetric.NewCounter("dns_query_fwd_error_name")
metricDNSFwdErrorNoUpstream = clientmetric.NewCounter("dns_query_fwd_error_no_upstream")
metricDNSFwdSuccess = clientmetric.NewCounter("dns_query_fwd_success")
metricDNSFwdErrorContext = clientmetric.NewCounter("dns_query_fwd_error_context")
metricDNSFwdErrorContextGotError = clientmetric.NewCounter("dns_query_fwd_error_context_got_error")
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
metricDNSFwdErrorParseAddr = clientmetric.NewCounter("dns_query_fwd_error_parse_addr")
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")
metricDNSFwdDoHErrorCT = clientmetric.NewCounter("dns_query_fwd_doh_error_content_type")
metricDNSFwdDoHErrorTransport = clientmetric.NewCounter("dns_query_fwd_doh_error_transport")
metricDNSFwdDoHErrorBody = clientmetric.NewCounter("dns_query_fwd_doh_error_body")
metricDNSResolveLocal = clientmetric.NewCounter("dns_resolve_local")
metricDNSResolveLocalErrorOnion = clientmetric.NewCounter("dns_resolve_local_error_onion")
metricDNSResolveLocalErrorMissing = clientmetric.NewCounter("dns_resolve_local_error_missing")
metricDNSResolveLocalErrorRefused = clientmetric.NewCounter("dns_resolve_local_error_refused")
metricDNSResolveLocalOKA = clientmetric.NewCounter("dns_resolve_local_ok_a")
metricDNSResolveLocalOKAAAA = clientmetric.NewCounter("dns_resolve_local_ok_aaaa")
metricDNSResolveLocalOKAll = clientmetric.NewCounter("dns_resolve_local_ok_all")
metricDNSResolveLocalNoA = clientmetric.NewCounter("dns_resolve_local_no_a")
metricDNSResolveLocalNoAAAA = clientmetric.NewCounter("dns_resolve_local_no_aaaa")
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
)

View File

@@ -6,6 +6,7 @@ package resolver
import (
"fmt"
"net"
"strings"
"testing"
@@ -179,6 +180,129 @@ var resolveToNXDOMAIN = dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg)
w.WriteMsg(m)
})
// weirdoGoCNAMEHandler returns a DNS handler that satisfies
// Go's weird Resolver.LookupCNAME (read its godoc carefully!).
//
// This doesn't even return a CNAME record, because that's not
// what Go looks for.
func weirdoGoCNAMEHandler(target string) dns.HandlerFunc {
return func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
question := req.Question[0]
switch question.Qtype {
case dns.TypeA:
m.Answer = append(m.Answer, &dns.CNAME{
Hdr: dns.RR_Header{
Name: target,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 600,
},
Target: target,
})
case dns.TypeAAAA:
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: target,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: 600,
},
AAAA: net.ParseIP("1::2"),
})
}
w.WriteMsg(m)
}
}
// dnsHandler returns a handler that replies with the answers/options
// provided.
//
// Types supported: netaddr.IP.
func dnsHandler(answers ...interface{}) 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.RecursionAvailable = true // to stop net package's errLameReferral on empty replies
question := req.Question[0]
for _, a := range answers {
switch a := a.(type) {
default:
panic(fmt.Sprintf("unsupported dnsHandler arg %T", a))
case netaddr.IP:
ip := a
if ip.Is4() {
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: ip.IPAddr().IP,
})
} else if ip.Is6() {
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
},
AAAA: ip.IPAddr().IP,
})
}
case dns.PTR:
ptr := a
ptr.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &ptr)
case dns.CNAME:
c := a
c.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 600,
}
m.Answer = append(m.Answer, &c)
case dns.TXT:
txt := a
txt.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &txt)
case dns.SRV:
srv := a
srv.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &srv)
case dns.NS:
rr := a
rr.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &rr)
}
}
w.WriteMsg(m)
}
}
func serveDNS(tb testing.TB, addr string, records ...interface{}) *dns.Server {
if len(records)%2 != 0 {
panic("must have an even number of record values")

View File

@@ -6,18 +6,25 @@ package resolver
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
miekdns "github.com/miekg/dns"
"golang.org/x/net/dns/dnsmessage"
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/net/tsdial"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
@@ -34,14 +41,16 @@ var (
var dnsCfg = Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{
"test1.ipn.dev.": []netaddr.IP{testipv4},
"test2.ipn.dev.": []netaddr.IP{testipv6},
"test1.ipn.dev.": {testipv4},
"test2.ipn.dev.": {testipv6},
},
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
}
const noEdns = 0
const dnsHeaderLen = 12
func dnspacket(domain dnsname.FQDN, tp dns.Type, ednsSize uint16) []byte {
var dnsHeader dns.Header
question := dns.Question{
@@ -308,7 +317,7 @@ func TestRDNSNameToIPv6(t *testing.T) {
}
func newResolver(t testing.TB) *Resolver {
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */)
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */, new(tsdial.Dialer))
}
func TestResolveLocal(t *testing.T) {
@@ -1062,7 +1071,7 @@ func TestForwardLinkSelection(t *testing.T) {
return "special"
}
return ""
}))
}), new(tsdial.Dialer))
// Test non-special IP.
if got, err := fwd.packetListener(netaddr.IP{}); err != nil {
@@ -1092,3 +1101,383 @@ func TestForwardLinkSelection(t *testing.T) {
type linkSelFunc func(ip netaddr.IP) string
func (f linkSelFunc) PickLink(ip netaddr.IP) string { return f(ip) }
func TestHandleExitNodeDNSQueryWithNetPkg(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows; waiting for golang.org/issue/33097")
}
records := []interface{}{
"no-records.test.",
dnsHandler(),
"one-a.test.",
dnsHandler(netaddr.MustParseIP("1.2.3.4")),
"two-a.test.",
dnsHandler(netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("5.6.7.8")),
"one-aaaa.test.",
dnsHandler(netaddr.MustParseIP("1::2")),
"two-aaaa.test.",
dnsHandler(netaddr.MustParseIP("1::2"), netaddr.MustParseIP("3::4")),
"nx-domain.test.",
resolveToNXDOMAIN,
"4.3.2.1.in-addr.arpa.",
dnsHandler(miekdns.PTR{Ptr: "foo.com."}),
"cname.test.",
weirdoGoCNAMEHandler("the-target.foo."),
"txt.test.",
dnsHandler(
miekdns.TXT{Txt: []string{"txt1=one"}},
miekdns.TXT{Txt: []string{"txt2=two"}},
miekdns.TXT{Txt: []string{"txt3=three"}},
),
"srv.test.",
dnsHandler(
miekdns.SRV{
Priority: 1,
Weight: 2,
Port: 3,
Target: "foo.com.",
},
miekdns.SRV{
Priority: 4,
Weight: 5,
Port: 6,
Target: "bar.com.",
},
),
"ns.test.",
dnsHandler(miekdns.NS{Ns: "ns1.foo."}, miekdns.NS{Ns: "ns2.bar."}),
}
v4server := serveDNS(t, "127.0.0.1:0", records...)
defer v4server.Shutdown()
// backendResolver is the resolver between
// handleExitNodeDNSQueryWithNetPkg and its upstream resolver,
// which in this test's case is the miekg/dns test DNS server
// (v4server).
backResolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "udp", v4server.PacketConn.LocalAddr().String())
},
}
t.Run("no_such_host", func(t *testing.T) {
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
Header: dnsmessage.Header{
ID: 123,
Response: true,
OpCode: 0, // query
},
Question: dnsmessage.Question{
Name: dnsmessage.MustNewName("nx-domain.test."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
})
if err != nil {
t.Fatal(err)
}
if len(res) < dnsHeaderLen {
t.Fatal("short reply")
}
rcode := dns.RCode(res[3] & 0x0f)
if rcode != dns.RCodeNameError {
t.Errorf("RCode = %v; want dns.RCodeNameError", rcode)
t.Logf("Response was: %q", res)
}
})
matchPacked := func(want string) func(t testing.TB, got []byte) {
return func(t testing.TB, got []byte) {
if string(got) == want {
return
}
t.Errorf("unexpected reply.\n got: %q\nwant: %q\n", got, want)
t.Errorf("\nin hex:\n got: % 2x\nwant: % 2x\n", got, want)
}
}
tests := []struct {
Type dnsmessage.Type
Name string
Check func(t testing.TB, got []byte)
}{
{
Type: dnsmessage.TypeA,
Name: "one-a.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05one-a\x04test\x00\x00\x01\x00\x01\x05one-a\x04test\x00\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04"),
},
{
Type: dnsmessage.TypeA,
Name: "two-a.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x05two-a\x04test\x00\x00\x01\x00\x01\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x05\x06\a\b"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "one-aaaa.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\bone-aaaa\x04test\x00\x00\x1c\x00\x01\bone-aaaa\x04test\x00\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "two-aaaa.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\btwo-aaaa\x04test\x00\x00\x1c\x00\x01\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04"),
},
{
Type: dnsmessage.TypePTR,
Name: "4.3.2.1.in-addr.arpa.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x00\x00\x02X\x00\t\x03foo\x03com\x00"),
},
{
Type: dnsmessage.TypeCNAME,
Name: "cname.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05cname\x04test\x00\x00\x05\x00\x01\x05cname\x04test\x00\x00\x05\x00\x01\x00\x00\x02X\x00\x10\nthe-target\x03foo\x00"),
},
// No records of various types
{
Type: dnsmessage.TypeA,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x01\x00\x01"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x1c\x00\x01"),
},
{
Type: dnsmessage.TypeCNAME,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x05\x00\x01"),
},
{
Type: dnsmessage.TypeSRV,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00!\x00\x01"),
},
{
Type: dnsmessage.TypeTXT,
Name: "txt.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x03\x00\x00\x00\x00\x03txt\x04test\x00\x00\x10\x00\x01\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt1=one\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt2=two\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\v\ntxt3=three"),
},
{
Type: dnsmessage.TypeSRV,
Name: "srv.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x03srv\x04test\x00\x00!\x00\x01\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x01\x00\x02\x00\x03\x03foo\x03com\x00\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x04\x00\x05\x00\x06\x03bar\x03com\x00"),
},
{
Type: dnsmessage.TypeNS,
Name: "ns.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x02ns\x04test\x00\x00\x02\x00\x01\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns1\x03foo\x00\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns2\x03bar\x00"),
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v_%v", tt.Type, strings.Trim(tt.Name, ".")), func(t *testing.T) {
got, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
Header: dnsmessage.Header{
ID: 123,
Response: true,
OpCode: 0, // query
},
Question: dnsmessage.Question{
Name: dnsmessage.MustNewName(tt.Name),
Type: tt.Type,
Class: dnsmessage.ClassINET,
},
})
if err != nil {
t.Fatal(err)
}
if len(got) < dnsHeaderLen {
t.Errorf("short record")
}
if tt.Check != nil {
tt.Check(t, got)
if t.Failed() {
t.Errorf("Got: %q\nIn hex: % 02x", got, got)
}
}
})
}
wrapRes := newWrapResolver(backResolver)
ctx := context.Background()
t.Run("wrap_ip_a", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "two-a.test.")
if err != nil {
t.Fatal(err)
}
if got, want := ips, []net.IP{
net.ParseIP("1.2.3.4").To4(),
net.ParseIP("5.6.7.8").To4(),
}; !reflect.DeepEqual(got, want) {
t.Errorf("LookupIP = %v; want %v", got, want)
}
})
t.Run("wrap_ip_aaaa", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "two-aaaa.test.")
if err != nil {
t.Fatal(err)
}
if got, want := ips, []net.IP{
net.ParseIP("1::2"),
net.ParseIP("3::4"),
}; !reflect.DeepEqual(got, want) {
t.Errorf("LookupIP(v6) = %v; want %v", got, want)
}
})
t.Run("wrap_ip_nx", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "nx-domain.test.")
if !isGoNoSuchHostError(err) {
t.Errorf("no NX domain = (%v, %v); want no host error", ips, err)
}
})
t.Run("wrap_srv", func(t *testing.T) {
_, srvs, err := wrapRes.LookupSRV(ctx, "", "", "srv.test.")
if err != nil {
t.Fatal(err)
}
if got, want := srvs, []*net.SRV{
{
Target: "foo.com.",
Priority: 1,
Weight: 2,
Port: 3,
},
{
Target: "bar.com.",
Priority: 4,
Weight: 5,
Port: 6,
},
}; !reflect.DeepEqual(got, want) {
jgot, _ := json.Marshal(got)
jwant, _ := json.Marshal(want)
t.Errorf("SRV = %s; want %s", jgot, jwant)
}
})
t.Run("wrap_txt", func(t *testing.T) {
txts, err := wrapRes.LookupTXT(ctx, "txt.test.")
if err != nil {
t.Fatal(err)
}
if got, want := txts, []string{"txt1=one", "txt2=two", "txt3=three"}; !reflect.DeepEqual(got, want) {
t.Errorf("TXT = %q; want %q", got, want)
}
})
t.Run("wrap_ns", func(t *testing.T) {
nss, err := wrapRes.LookupNS(ctx, "ns.test.")
if err != nil {
t.Fatal(err)
}
if got, want := nss, []*net.NS{
{Host: "ns1.foo."},
{Host: "ns2.bar."},
}; !reflect.DeepEqual(got, want) {
jgot, _ := json.Marshal(got)
jwant, _ := json.Marshal(want)
t.Errorf("NS = %s; want %s", jgot, jwant)
}
})
}
// newWrapResolver returns a resolver that uses r (via handleExitNodeDNSQueryWithNetPkg)
// to make DNS requests.
func newWrapResolver(r *net.Resolver) *net.Resolver {
if runtime.GOOS == "windows" {
panic("doesn't work on Windows") // golang.org/issue/33097
}
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return &wrapResolverConn{ctx: ctx, r: r}, nil
},
}
}
type wrapResolverConn struct {
ctx context.Context
r *net.Resolver
buf bytes.Buffer
}
var _ net.PacketConn = (*wrapResolverConn)(nil)
func (*wrapResolverConn) Close() error { return nil }
func (*wrapResolverConn) LocalAddr() net.Addr { return fakeAddr{} }
func (*wrapResolverConn) RemoteAddr() net.Addr { return fakeAddr{} }
func (*wrapResolverConn) SetDeadline(t time.Time) error { return nil }
func (*wrapResolverConn) SetReadDeadline(t time.Time) error { return nil }
func (*wrapResolverConn) SetWriteDeadline(t time.Time) error { return nil }
func (a *wrapResolverConn) Read(p []byte) (n int, err error) {
n, _, err = a.ReadFrom(p)
return
}
func (a *wrapResolverConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = a.buf.Read(p)
return n, fakeAddr{}, err
}
func (a *wrapResolverConn) Write(packet []byte) (n int, err error) {
return a.WriteTo(packet, fakeAddr{})
}
func (a *wrapResolverConn) WriteTo(q []byte, _ net.Addr) (n int, err error) {
resp := parseExitNodeQuery(q)
if resp == nil {
return 0, errors.New("bad query")
}
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), a.r, resp)
if err != nil {
return 0, err
}
a.buf.Write(res)
return len(q), nil
}
type fakeAddr struct{}
func (fakeAddr) Network() string { return "unused" }
func (fakeAddr) String() string { return "unused-todoAddr" }
func TestUnARPA(t *testing.T) {
tests := []struct {
in, want string
}{
{"", ""},
{"bad", ""},
{"4.4.8.8.in-addr.arpa.", "8.8.4.4"},
{".in-addr.arpa.", ""},
{"e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.", "2607:f8b0:400a:80b::200e"},
{".ip6.arpa.", ""},
}
for _, tt := range tests {
got, ok := unARPA(tt.in)
if ok != (got != "") {
t.Errorf("inconsistent results for %q: (%q, %v)", tt.in, got, ok)
}
if got != tt.want {
t.Errorf("unARPA(%q) = %q; want %q", tt.in, got, tt.want)
}
}
}

56
net/dns/utf.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dns
// This code is only used in Windows builds, but is in an
// OS-independent file so tests can run all the time.
import (
"bytes"
"encoding/binary"
"unicode/utf16"
)
// maybeUnUTF16 tries to detect whether bs contains UTF-16, and if so
// translates it to regular UTF-8.
//
// Some of wsl.exe's output get printed as UTF-16, which breaks a
// bunch of things. Try to detect this by looking for a zero byte in
// the first few bytes of output (which will appear if any of those
// codepoints are basic ASCII - very likely). From that we can infer
// that UTF-16 is being printed, and the byte order in use, and we
// decode that back to UTF-8.
//
// https://github.com/microsoft/WSL/issues/4607
func maybeUnUTF16(bs []byte) []byte {
if len(bs)%2 != 0 {
// Can't be complete UTF-16.
return bs
}
checkLen := 20
if len(bs) < checkLen {
checkLen = len(bs)
}
zeroOff := bytes.IndexByte(bs[:checkLen], 0)
if zeroOff == -1 {
return bs
}
// We assume wsl.exe is trying to print an ASCII codepoint,
// meaning the zero byte is in the upper 8 bits of the
// codepoint. That means we can use the zero's byte offset to
// work out if we're seeing little-endian or big-endian
// UTF-16.
var endian binary.ByteOrder = binary.LittleEndian
if zeroOff%2 == 0 {
endian = binary.BigEndian
}
var u16 []uint16
for i := 0; i < len(bs); i += 2 {
u16 = append(u16, endian.Uint16(bs[i:]))
}
return []byte(string(utf16.Decode(u16)))
}

25
net/dns/utf_test.go Normal file
View File

@@ -0,0 +1,25 @@
// 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 dns
import "testing"
func TestMaybeUnUTF16(t *testing.T) {
tests := []struct {
in string
want string
}{
{"abc", "abc"}, // UTF-8
{"a\x00b\x00c\x00", "abc"}, // UTF-16-LE
{"\x00a\x00b\x00c", "abc"}, // UTF-16-BE
}
for _, test := range tests {
got := string(maybeUnUTF16([]byte(test.in)))
if got != test.want {
t.Errorf("maybeUnUTF16(%q) = %q, want %q", test.in, got, test.want)
}
}
}

View File

@@ -6,13 +6,13 @@ package dns
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
"syscall"
"unicode/utf16"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
@@ -26,29 +26,7 @@ func wslDistros() ([]string, error) {
return nil, fmt.Errorf("%v: %q", err, string(b))
}
// The first line of output is a WSL header. E.g.
//
// C:\tsdev>wsl.exe -l
// Windows Subsystem for Linux Distributions:
// Ubuntu-20.04 (Default)
//
// We can skip it by passing '-q', but here we put it to work.
// It turns out wsl.exe -l is broken, and outputs UTF-16 names
// that nothing can read. (Try `wsl.exe -l | more`.)
// So we look at the header to see if it's UTF-16.
// If so, we run the rest through a UTF-16 parser.
//
// https://github.com/microsoft/WSL/issues/4607
var output string
if bytes.HasPrefix(b, []byte("W\x00i\x00n\x00d\x00o\x00w\x00s\x00")) {
output, err = decodeUTF16(b)
if err != nil {
return nil, fmt.Errorf("failed to decode wsl.exe -l output %q: %v", b, err)
}
} else {
output = string(b)
}
lines := strings.Split(output, "\n")
lines := strings.Split(string(b), "\n")
if len(lines) < 1 {
return nil, nil
}
@@ -66,19 +44,6 @@ func wslDistros() ([]string, error) {
return distros, nil
}
func decodeUTF16(b []byte) (string, error) {
if len(b) == 0 {
return "", nil
} else if len(b)%2 != 0 {
return "", fmt.Errorf("decodeUTF16: invalid length %d", len(b))
}
var u16 []uint16
for i := 0; i < len(b); i += 2 {
u16 = append(u16, uint16(b[i])+(uint16(b[i+1])<<8))
}
return string(utf16.Decode(u16)), nil
}
// wslManager is a DNS manager for WSL2 linux distributions.
// It configures /etc/wsl.conf and /etc/resolv.conf.
type wslManager struct {
@@ -193,7 +158,8 @@ func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 064
func (fs wslFS) ReadFile(name string) ([]byte, error) {
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
var ee *exec.ExitError
if errors.As(err, &ee) && ee.ExitCode() == 1 {
return nil, os.ErrNotExist
}
return b, err
@@ -225,7 +191,10 @@ func wslCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
cmd.Stdout = buf
cmd.Stderr = buf
err := wslRun(cmd)
return buf.Bytes(), err
if err != nil {
return nil, err
}
return maybeUnUTF16(buf.Bytes()), nil
}
func wslRun(cmd *exec.Cmd) (err error) {

View File

@@ -0,0 +1,314 @@
// 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 dnscache
import (
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/golang/groupcache/lru"
"golang.org/x/net/dns/dnsmessage"
)
// MessageCache is a cache that works at the DNS message layer,
// with its cache keyed on a DNS wire-level question, and capable
// of replying to DNS messages.
//
// Its zero value is ready for use with a default cache size.
// Use SetMaxCacheSize to specify the cache size.
//
// It's safe for concurrent use.
type MessageCache struct {
// Clock is a clock, for testing.
// If nil, time.Now is used.
Clock func() time.Time
mu sync.Mutex
cacheSizeSet int // 0 means default
cache lru.Cache // msgQ => *msgCacheValue
}
func (c *MessageCache) now() time.Time {
if c.Clock != nil {
return c.Clock()
}
return time.Now()
}
// SetMaxCacheSize sets the maximum number of DNS cache entries that
// can be stored.
func (c *MessageCache) SetMaxCacheSize(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.cacheSizeSet = n
c.pruneLocked()
}
// Flush clears the cache.
func (c *MessageCache) Flush() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Clear()
}
// pruneLocked prunes down the cache size to the configured (or
// default) max size.
func (c *MessageCache) pruneLocked() {
max := c.cacheSizeSet
if max == 0 {
max = 500
}
for c.cache.Len() > max {
c.cache.RemoveOldest()
}
}
// msgQ is the MessageCache cache key.
//
// It's basically a golang.org/x/net/dns/dnsmessage#Question but the
// Class is omitted (we only cache ClassINET) and we store a Go string
// instead of a 256 byte dnsmessage.Name array.
type msgQ struct {
Name string
Type dnsmessage.Type // A, AAAA, MX, etc
}
// A *msgCacheValue is the cached value for a msgQ (question) key.
//
// Despite using pointers for storage and methods, the value is
// immutable once placed in the cache.
type msgCacheValue struct {
Expires time.Time
// Answers are the minimum data to reconstruct a DNS response
// message. TTLs are added later when converting to a
// dnsmessage.Resource.
Answers []msgResource
}
type msgResource struct {
Name string
Type dnsmessage.Type // dnsmessage.UnknownResource.Type
Data []byte // dnsmessage.UnknownResource.Data
}
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
// when the request can not be satisified from cache.
var ErrCacheMiss = errors.New("cache miss")
var parserPool = &sync.Pool{
New: func() interface{} { return new(dnsmessage.Parser) },
}
// ReplyFromCache writes a DNS reply to w for the provided DNS query message,
// which must begin with the two ID bytes of a DNS message.
//
// If there's a cache miss, the message is invalid or unexpected,
// ErrCacheMiss is returned. On cache hit, either nil or an error from
// a w.Write call is returned.
func (c *MessageCache) ReplyFromCache(w io.Writer, dnsQueryMessage []byte) error {
cacheKey, txID, ok := getDNSQueryCacheKey(dnsQueryMessage)
if !ok {
return ErrCacheMiss
}
now := c.now()
c.mu.Lock()
cacheEntI, _ := c.cache.Get(cacheKey)
v, ok := cacheEntI.(*msgCacheValue)
if ok && now.After(v.Expires) {
c.cache.Remove(cacheKey)
ok = false
}
c.mu.Unlock()
if !ok {
return ErrCacheMiss
}
ttl := uint32(v.Expires.Sub(now).Seconds())
packedRes, err := packDNSResponse(cacheKey, txID, ttl, v.Answers)
if err != nil {
return ErrCacheMiss
}
_, err = w.Write(packedRes)
return err
}
var (
errNotCacheable = errors.New("question not cacheable")
)
// AddCacheEntry adds a cache entry to the cache.
// It returns an error if the entry could not be cached.
func (c *MessageCache) AddCacheEntry(qPacket, res []byte) error {
cacheKey, qID, ok := getDNSQueryCacheKey(qPacket)
if !ok {
return errNotCacheable
}
now := c.now()
v := &msgCacheValue{}
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
resh, err := p.Start(res)
if err != nil {
return fmt.Errorf("reading header in response: %w", err)
}
if resh.ID != qID {
return fmt.Errorf("response ID doesn't match query ID")
}
q, err := p.Question()
if err != nil {
return fmt.Errorf("reading 1st question in response: %w", err)
}
if _, err := p.Question(); err != dnsmessage.ErrSectionDone {
if err == nil {
return errors.New("unexpected 2nd question in response")
}
return fmt.Errorf("after reading 1st question in response: %w", err)
}
if resName := asciiLowerName(q.Name).String(); resName != cacheKey.Name {
return fmt.Errorf("response question name %q != question name %q", resName, cacheKey.Name)
}
for {
rh, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return fmt.Errorf("reading answer: %w", err)
}
res, err := p.UnknownResource()
if err != nil {
return fmt.Errorf("reading resource: %w", err)
}
if rh.Class != dnsmessage.ClassINET {
continue
}
// Set the cache entry's expiration to the soonest
// we've seen. (They should all be the same, though)
expires := now.Add(time.Duration(rh.TTL) * time.Second)
if v.Expires.IsZero() || expires.Before(v.Expires) {
v.Expires = expires
}
v.Answers = append(v.Answers, msgResource{
Name: rh.Name.String(),
Type: rh.Type,
Data: res.Data, // doesn't alias; a copy from dnsmessage.unpackUnknownResource
})
}
c.addCacheValue(cacheKey, v)
return nil
}
func (c *MessageCache) addCacheValue(cacheKey msgQ, v *msgCacheValue) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Add(cacheKey, v)
c.pruneLocked()
}
func getDNSQueryCacheKey(msg []byte) (cacheKey msgQ, txID uint16, ok bool) {
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
h, err := p.Start(msg)
const dnsHeaderSize = 12
if err != nil || h.OpCode != 0 || h.Response || h.Truncated ||
len(msg) < dnsHeaderSize { // p.Start checks this anyway, but to be explicit for slicing below
return cacheKey, 0, false
}
var (
numQ = binary.BigEndian.Uint16(msg[4:6])
numAns = binary.BigEndian.Uint16(msg[6:8])
numAuth = binary.BigEndian.Uint16(msg[8:10])
numAddn = binary.BigEndian.Uint16(msg[10:12])
)
_ = numAddn // ignore this for now; do client OSes send EDNS additional? assume so, ignore.
if !(numQ == 1 && numAns == 0 && numAuth == 0) {
// Something weird. We don't want to deal with it.
return cacheKey, 0, false
}
q, err := p.Question()
if err != nil {
// Already verified numQ == 1 so shouldn't happen, but:
return cacheKey, 0, false
}
if q.Class != dnsmessage.ClassINET {
// We only cache the Internet class.
return cacheKey, 0, false
}
return msgQ{Name: asciiLowerName(q.Name).String(), Type: q.Type}, h.ID, true
}
func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
nb := n.Data[:]
if int(n.Length) < len(n.Data) {
nb = nb[:n.Length]
}
for i, b := range nb {
if 'A' <= b && b <= 'Z' {
n.Data[i] += 0x20
}
}
return n
}
// packDNSResponse builds a DNS response for the given question and
// transaction ID. The response resource records will have have the
// same provided TTL.
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
var baseMem []byte // TODO: guess a max size based on looping over answers?
b := dnsmessage.NewBuilder(baseMem, dnsmessage.Header{
ID: txID,
Response: true,
OpCode: 0,
Authoritative: false,
Truncated: false,
RCode: dnsmessage.RCodeSuccess,
})
name, err := dnsmessage.NewName(q.Name)
if err != nil {
return nil, err
}
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(dnsmessage.Question{
Name: name,
Type: q.Type,
Class: dnsmessage.ClassINET,
}); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
for _, r := range answers {
name, err := dnsmessage.NewName(r.Name)
if err != nil {
return nil, err
}
if err := b.UnknownResource(dnsmessage.ResourceHeader{
Name: name,
Type: r.Type,
Class: dnsmessage.ClassINET,
TTL: ttl,
}, dnsmessage.UnknownResource{
Type: r.Type,
Data: r.Data,
}); err != nil {
return nil, err
}
}
return b.Finish()
}

View File

@@ -0,0 +1,292 @@
// 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 dnscache
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"runtime"
"testing"
"time"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/tstest"
)
func TestMessageCache(t *testing.T) {
clock := &tstest.Clock{
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
}
mc := &MessageCache{Clock: clock.Now}
mc.SetMaxCacheSize(2)
clock.Advance(time.Second)
var out bytes.Buffer
if err := mc.ReplyFromCache(&out, makeQ(1, "foo.com.")); err != ErrCacheMiss {
t.Fatalf("unexpected error: %v", err)
}
if err := mc.AddCacheEntry(
makeQ(2, "foo.com."),
makeRes(2, "FOO.COM.", ttlOpt(10),
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}},
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}})); err != nil {
t.Fatal(err)
}
// Expect cache hit, with 10 seconds remaining.
out.Reset()
if err := mc.ReplyFromCache(&out, makeQ(3, "foo.com.")); err != nil {
t.Fatalf("expected cache hit; got: %v", err)
}
if p := mustParseResponse(t, out.Bytes()); p.TxID != 3 {
t.Errorf("TxID = %v; want %v", p.TxID, 3)
} else if p.TTL != 10 {
t.Errorf("TTL = %v; want 10", p.TTL)
}
// One second elapses, expect a cache hit, with 9 seconds
// remaining.
clock.Advance(time.Second)
out.Reset()
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.")); err != nil {
t.Fatalf("expected cache hit; got: %v", err)
}
if p := mustParseResponse(t, out.Bytes()); p.TxID != 4 {
t.Errorf("TxID = %v; want %v", p.TxID, 4)
} else if p.TTL != 9 {
t.Errorf("TTL = %v; want 9", p.TTL)
}
// Expect cache miss on MX record.
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.TypeMX)); err != ErrCacheMiss {
t.Fatalf("expected cache miss on MX; got: %v", err)
}
// Expect cache miss on CHAOS class.
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.ClassCHAOS)); err != ErrCacheMiss {
t.Fatalf("expected cache miss on CHAOS; got: %v", err)
}
// Ten seconds elapses; expect a cache miss.
clock.Advance(10 * time.Second)
if err := mc.ReplyFromCache(&out, makeQ(5, "foo.com.")); err != ErrCacheMiss {
t.Fatalf("expected cache miss, got: %v", err)
}
}
type parsedMeta struct {
TxID uint16
TTL uint32
}
func mustParseResponse(t testing.TB, r []byte) (ret parsedMeta) {
t.Helper()
var p dnsmessage.Parser
h, err := p.Start(r)
if err != nil {
t.Fatal(err)
}
ret.TxID = h.ID
qq, err := p.AllQuestions()
if err != nil {
t.Fatalf("AllQuestions: %v", err)
}
if len(qq) != 1 {
t.Fatalf("num questions = %v; want 1", len(qq))
}
aa, err := p.AllAnswers()
if err != nil {
t.Fatalf("AllAnswers: %v", err)
}
for _, r := range aa {
if ret.TTL == 0 {
ret.TTL = r.Header.TTL
}
if ret.TTL != r.Header.TTL {
t.Fatal("mixed TTLs")
}
}
return ret
}
type responseOpt bool
type ttlOpt uint32
func makeQ(txID uint16, name string, opt ...interface{}) []byte {
opt = append(opt, responseOpt(false))
return makeDNSPkt(txID, name, opt...)
}
func makeRes(txID uint16, name string, opt ...interface{}) []byte {
opt = append(opt, responseOpt(true))
return makeDNSPkt(txID, name, opt...)
}
func makeDNSPkt(txID uint16, name string, opt ...interface{}) []byte {
typ := dnsmessage.TypeA
class := dnsmessage.ClassINET
var response bool
var answers []dnsmessage.ResourceBody
var ttl uint32 = 1 // one second by default
for _, o := range opt {
switch o := o.(type) {
case dnsmessage.Type:
typ = o
case dnsmessage.Class:
class = o
case responseOpt:
response = bool(o)
case dnsmessage.ResourceBody:
answers = append(answers, o)
case ttlOpt:
ttl = uint32(o)
default:
panic(fmt.Sprintf("unknown opt type %T", o))
}
}
qname := dnsmessage.MustNewName(name)
msg := dnsmessage.Message{
Header: dnsmessage.Header{ID: txID, Response: response},
Questions: []dnsmessage.Question{
{
Name: qname,
Type: typ,
Class: class,
},
},
}
for _, rb := range answers {
msg.Answers = append(msg.Answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: qname,
Type: typ,
Class: class,
TTL: ttl,
},
Body: rb,
})
}
buf, err := msg.Pack()
if err != nil {
panic(err)
}
return buf
}
func TestASCIILowerName(t *testing.T) {
n := asciiLowerName(dnsmessage.MustNewName("Foo.COM."))
if got, want := n.String(), "foo.com."; got != want {
t.Errorf("got = %q; want %q", got, want)
}
}
func TestGetDNSQueryCacheKey(t *testing.T) {
tests := []struct {
name string
pkt []byte
want msgQ
txID uint16
anyTX bool
}{
{
name: "empty",
},
{
name: "a",
pkt: makeQ(123, "foo.com."),
want: msgQ{"foo.com.", dnsmessage.TypeA},
txID: 123,
},
{
name: "aaaa",
pkt: makeQ(6, "foo.com.", dnsmessage.TypeAAAA),
want: msgQ{"foo.com.", dnsmessage.TypeAAAA},
txID: 6,
},
{
name: "normalize_case",
pkt: makeQ(123, "FoO.CoM."),
want: msgQ{"foo.com.", dnsmessage.TypeA},
txID: 123,
},
{
name: "ignore_response",
pkt: makeRes(123, "foo.com."),
},
{
name: "ignore_question_with_answers",
pkt: makeQ(2, "foo.com.", &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}),
},
{
name: "whatever_go_generates", // in case Go's net package grows functionality we don't handle
pkt: getGoNetPacketDNSQuery("from-go.foo."),
want: msgQ{"from-go.foo.", dnsmessage.TypeA},
anyTX: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotTX, ok := getDNSQueryCacheKey(tt.pkt)
if !ok {
if tt.txID == 0 && got == (msgQ{}) {
return
}
t.Fatal("failed")
}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
if gotTX != tt.txID && !tt.anyTX {
t.Errorf("got tx %v, want %v", gotTX, tt.txID)
}
})
}
}
func getGoNetPacketDNSQuery(name string) []byte {
if runtime.GOOS == "windows" {
// On Windows, Go's net.Resolver doesn't use the DNS client.
// See https://github.com/golang/go/issues/33097 which
// was approved but not yet implemented.
// For now just pretend it's implemented to make this test
// pass on Windows with complicated the caller.
return makeQ(123, name)
}
res := make(chan []byte, 1)
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return goResolverConn(res), nil
},
}
r.LookupIP(context.Background(), "ip4", name)
return <-res
}
type goResolverConn chan<- []byte
func (goResolverConn) Close() error { return nil }
func (goResolverConn) LocalAddr() net.Addr { return todoAddr{} }
func (goResolverConn) RemoteAddr() net.Addr { return todoAddr{} }
func (goResolverConn) SetDeadline(t time.Time) error { return nil }
func (goResolverConn) SetReadDeadline(t time.Time) error { return nil }
func (goResolverConn) SetWriteDeadline(t time.Time) error { return nil }
func (goResolverConn) Read([]byte) (int, error) { return 0, errors.New("boom") }
func (c goResolverConn) Write(p []byte) (int, error) {
select {
case c <- p[2:]: // skip 2 byte length for TCP mode DNS query
default:
}
return 0, errors.New("boom")
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }

View File

@@ -90,7 +90,7 @@ func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) {
// serverName and serverIP of are, say, "derpN.tailscale.com".
// queryName is the name being sought (e.g. "controlplane.tailscale.com"), passed as hint.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netaddr.IP, queryName string) (dnsMap, error) {
dialer := netns.NewDialer()
dialer := netns.NewDialer(log.Printf)
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {

View File

@@ -345,9 +345,18 @@ func (s *State) String() string {
return sb.String()
}
// An InterfaceFilter indicates whether EqualFiltered should use i when deciding whether two States are equal.
// ips are all the IPPrefixes associated with i.
type InterfaceFilter func(i Interface, ips []netaddr.IPPrefix) bool
// An IPFilter indicates whether EqualFiltered should use ip when deciding whether two States are equal.
// ip is an ip address associated with some interface under consideration.
type IPFilter func(ip netaddr.IP) bool
// EqualFiltered reports whether s and s2 are equal,
// considering only interfaces in s for which filter returns true.
func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.IPPrefix) bool) bool {
// considering only interfaces in s for which filter returns true,
// and considering only IPs for those interfaces for which filterIP returns true.
func (s *State) EqualFiltered(s2 *State, useInterface InterfaceFilter, useIP IPFilter) bool {
if s == nil && s2 == nil {
return true
}
@@ -364,7 +373,7 @@ func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.
}
for iname, i := range s.Interface {
ips := s.InterfaceIPs[iname]
if !filter(i, ips) {
if !useInterface(i, ips) {
continue
}
i2, ok := s2.Interface[iname]
@@ -375,7 +384,7 @@ func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.
if !ok {
return false
}
if !interfacesEqual(i, i2) || !prefixesEqual(ips, ips2) {
if !interfacesEqual(i, i2) || !prefixesEqualFiltered(ips, ips2, useIP) {
return false
}
}
@@ -390,6 +399,21 @@ func interfacesEqual(a, b Interface) bool {
bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))
}
func filteredIPPs(ipps []netaddr.IPPrefix, useIP IPFilter) []netaddr.IPPrefix {
// TODO: rewrite prefixesEqualFiltered to avoid making copies
x := make([]netaddr.IPPrefix, 0, len(ipps))
for _, ipp := range ipps {
if useIP(ipp.IP()) {
x = append(x, ipp)
}
}
return x
}
func prefixesEqualFiltered(a, b []netaddr.IPPrefix, useIP IPFilter) bool {
return prefixesEqual(filteredIPPs(a, useIP), filteredIPPs(b, useIP))
}
func prefixesEqual(a, b []netaddr.IPPrefix) bool {
if len(a) != len(b) {
return false
@@ -402,13 +426,24 @@ func prefixesEqual(a, b []netaddr.IPPrefix) bool {
return true
}
// FilterInteresting reports whether i is an interesting non-Tailscale interface.
func FilterInteresting(i Interface, ips []netaddr.IPPrefix) bool {
// UseInterestingInterfaces is an InterfaceFilter that reports whether i is an interesting interface.
// An interesting interface if it is (a) not owned by Tailscale and (b) routes interesting IP addresses.
// See UseInterestingIPs for the defition of an interesting IP address.
func UseInterestingInterfaces(i Interface, ips []netaddr.IPPrefix) bool {
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
}
// FilterAll always returns true, to use EqualFiltered against all interfaces.
func FilterAll(i Interface, ips []netaddr.IPPrefix) bool { return true }
// UseInterestingIPs is an IPFilter that reports whether ip is an interesting IP address.
// An IP address is interesting if it is neither a lopback not a link local unicast IP address.
func UseInterestingIPs(ip netaddr.IP) bool {
return isInterestingIP(ip)
}
// UseAllInterfaces is an InterfaceFilter that includes all interfaces.
func UseAllInterfaces(i Interface, ips []netaddr.IPPrefix) bool { return true }
// UseAllIPs is an IPFilter that includes all all IPs.
func UseAllIPs(ips netaddr.IP) bool { return true }
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
@@ -594,10 +629,7 @@ func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
// should log in interfaces.State logging. We don't need to show
// localhost or link-local addresses.
func isInterestingIP(ip netaddr.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return false
}
return true
return !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
}
var altNetInterfaces func() ([]Interface, error)

View File

@@ -6,6 +6,7 @@ package interfaces
import (
"encoding/json"
"net"
"testing"
"inet.af/netaddr"
@@ -28,7 +29,7 @@ func TestGetState(t *testing.T) {
t.Fatal(err)
}
if !st.EqualFiltered(st2, FilterAll) {
if !st.EqualFiltered(st2, UseAllInterfaces, UseAllIPs) {
// let's assume nobody was changing the system network interfaces between
// the two GetState calls.
t.Fatal("two States back-to-back were not equal")
@@ -68,3 +69,38 @@ func TestIsUsableV6(t *testing.T) {
}
}
}
func TestStateEqualFilteredIPFilter(t *testing.T) {
// s1 and s2 are identical, except that an "interesting" interface
// has gained an "uninteresting" IP address.
s1 := &State{
InterfaceIPs: map[string][]netaddr.IPPrefix{"x": {
netaddr.MustParseIPPrefix("42.0.0.0/8"),
netaddr.MustParseIPPrefix("169.254.0.0/16"), // link local unicast
}},
Interface: map[string]Interface{"x": {Interface: &net.Interface{Name: "x"}}},
}
s2 := &State{
InterfaceIPs: map[string][]netaddr.IPPrefix{"x": {
netaddr.MustParseIPPrefix("42.0.0.0/8"),
netaddr.MustParseIPPrefix("169.254.0.0/16"), // link local unicast
netaddr.MustParseIPPrefix("127.0.0.0/8"), // loopback (added)
}},
Interface: map[string]Interface{"x": {Interface: &net.Interface{Name: "x"}}},
}
// s1 and s2 are different...
if s1.EqualFiltered(s2, UseAllInterfaces, UseAllIPs) {
t.Errorf("%+v != %+v", s1, s2)
}
// ...and they look different if you only restrict to interesting interfaces...
if s1.EqualFiltered(s2, UseInterestingInterfaces, UseAllIPs) {
t.Errorf("%+v != %+v when restricting to interesting interfaces _but not_ IPs", s1, s2)
}
// ...but because the additional IP address is uninteresting, we should treat them as the same.
if !s1.EqualFiltered(s2, UseInterestingInterfaces, UseInterestingIPs) {
t.Errorf("%+v == %+v when restricting to interesting interfaces and IPs", s1, s2)
}
}

View File

@@ -116,8 +116,12 @@ 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)
if iface.IfType != winipcfg.IfTypePropVirtual {
return true
}
desc := iface.Description()
return !(strings.Contains(desc, tsconst.WintunInterfaceDesc) ||
strings.Contains(desc, tsconst.WintunInterfaceDesc0_14))
}
// NonTailscaleInterfaces returns a map of interface LUID to interface

View File

@@ -34,6 +34,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/clientmetric"
)
// Debugging and experimentation tweakables.
@@ -232,6 +233,12 @@ func (c *Client) MakeNextReportFull() {
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
c.vlogf("received STUN packet from %s", src)
if src.IP().Is4() {
metricSTUNRecv4.Add(1)
} else if src.IP().Is6() {
metricSTUNRecv6.Add(1)
}
c.mu.Lock()
if c.handleHairSTUNLocked(pkt, src) {
c.mu.Unlock()
@@ -737,7 +744,13 @@ func (c *Client) udpBindAddr() string {
// GetReport gets a report.
//
// It may not be called concurrently with itself.
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) {
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, reterr error) {
defer func() {
if reterr != nil {
metricNumGetReportError.Add(1)
}
}()
metricNumGetReport.Add(1)
// Mask user context with ours that we guarantee to cancel so
// we can depend on it being closed in goroutines later.
// (User ctx might be context.Background, etc)
@@ -769,6 +782,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
last = nil // causes makeProbePlan below to do a full (initial) plan
c.nextFull = false
c.lastFull = now
metricNumGetReportFull.Add(1)
}
rs.incremental = last != nil
c.mu.Unlock()
@@ -793,7 +807,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
// Create a UDP4 socket used for sending to our discovered IPv4 address.
rs.pc4Hair, err = netns.Listener().ListenPacket(ctx, "udp4", ":0")
rs.pc4Hair, err = netns.Listener(c.logf).ListenPacket(ctx, "udp4", ":0")
if err != nil {
c.logf("udp4: %v", err)
return nil, err
@@ -821,7 +835,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn4; f != nil {
rs.pc4 = f()
} else {
u4, err := netns.Listener().ListenPacket(ctx, "udp4", c.udpBindAddr())
u4, err := netns.Listener(c.logf).ListenPacket(ctx, "udp4", c.udpBindAddr())
if err != nil {
c.logf("udp4: %v", err)
return nil, err
@@ -834,7 +848,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn6; f != nil {
rs.pc6 = f()
} else {
u6, err := netns.Listener().ListenPacket(ctx, "udp6", c.udpBindAddr())
u6, err := netns.Listener(c.logf).ListenPacket(ctx, "udp6", c.udpBindAddr())
if err != nil {
c.logf("udp6: %v", err)
} else {
@@ -983,6 +997,7 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
}
func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) {
metricHTTPSend.Add(1)
var result httpstat.Result
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), overallProbeTimeout)
defer cancel()
@@ -1217,6 +1232,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
switch probe.proto {
case probeIPv4:
metricSTUNSend4.Add(1)
n, err := rs.pc4.WriteTo(req, addr)
if n == len(req) && err == nil {
rs.mu.Lock()
@@ -1224,6 +1240,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
rs.mu.Unlock()
}
case probeIPv6:
metricSTUNSend6.Add(1)
n, err := rs.pc6.WriteTo(req, addr)
if n == len(req) && err == nil {
rs.mu.Lock()
@@ -1322,3 +1339,15 @@ func conciseOptBool(b opt.Bool, trueVal string) string {
}
return ""
}
var (
metricNumGetReport = clientmetric.NewCounter("netcheck_report")
metricNumGetReportFull = clientmetric.NewCounter("netcheck_report_full")
metricNumGetReportError = clientmetric.NewCounter("netcheck_report_error")
metricSTUNSend4 = clientmetric.NewCounter("netcheck_stun_send_ipv4")
metricSTUNSend6 = clientmetric.NewCounter("netcheck_stun_send_ipv6")
metricSTUNRecv4 = clientmetric.NewCounter("netcheck_stun_recv_ipv4")
metricSTUNRecv6 = clientmetric.NewCounter("netcheck_stun_recv_ipv6")
metricHTTPSend = clientmetric.NewCounter("netcheck_https_measure")
)

View File

@@ -21,6 +21,7 @@ import (
"inet.af/netaddr"
"tailscale.com/net/netknob"
"tailscale.com/syncs"
"tailscale.com/types/logger"
)
var disabled syncs.AtomicBool
@@ -34,19 +35,19 @@ func SetEnabled(on bool) {
// Listener returns a new net.Listener with its Control hook func
// initialized as necessary to run in logical network namespace that
// doesn't route back into Tailscale.
func Listener() *net.ListenConfig {
func Listener(logf logger.Logf) *net.ListenConfig {
if disabled.Get() {
return new(net.ListenConfig)
}
return &net.ListenConfig{Control: control}
return &net.ListenConfig{Control: control(logf)}
}
// NewDialer returns a new Dialer using a net.Dialer with its Control
// hook func initialized as necessary to run in a logical network
// namespace that doesn't route back into Tailscale. It also handles
// using a SOCKS if configured in the environment with ALL_PROXY.
func NewDialer() Dialer {
return FromDialer(&net.Dialer{
func NewDialer(logf logger.Logf) Dialer {
return FromDialer(logf, &net.Dialer{
KeepAlive: netknob.PlatformTCPKeepAlive(),
})
}
@@ -55,11 +56,11 @@ func NewDialer() Dialer {
// network namespace that doesn't route back into Tailscale. It also
// handles using a SOCKS if configured in the environment with
// ALL_PROXY.
func FromDialer(d *net.Dialer) Dialer {
func FromDialer(logf logger.Logf, d *net.Dialer) Dialer {
if disabled.Get() {
return d
}
d.Control = control
d.Control = control(logf)
if wrapDialer != nil {
return wrapDialer(d)
}

View File

@@ -11,6 +11,8 @@ import (
"fmt"
"sync"
"syscall"
"tailscale.com/types/logger"
)
var (
@@ -44,11 +46,15 @@ func SetAndroidProtectFunc(f func(fd int) error) {
androidProtectFunc = f
}
// control marks c as necessary to dial in a separate network namespace.
func control(logger.Logf) func(network, address string, c syscall.RawConn) error {
return controlC
}
// controlC marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control.
func control(network, address string, c syscall.RawConn) error {
func controlC(network, address string, c syscall.RawConn) error {
var sockErr error
err := c.Control(func(fd uintptr) {
androidProtectFuncMu.Lock()

View File

@@ -9,26 +9,32 @@ package netns
import (
"fmt"
"log"
"strings"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
"tailscale.com/types/logger"
)
// control marks c as necessary to dial in a separate network namespace.
func control(logf logger.Logf) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) error {
return controlLogf(logf, network, address, c)
}
}
// controlLogf marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control.
func control(network, address string, c syscall.RawConn) error {
func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) error {
if strings.HasPrefix(address, "127.") || address == "::1" {
// Don't bind to an interface for localhost connections.
return nil
}
idx, err := interfaces.DefaultRouteInterfaceIndex()
if err != nil {
log.Printf("netns: DefaultRouteInterfaceIndex: %v", err)
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
return nil
}
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
@@ -47,7 +53,7 @@ func control(network, address string, c syscall.RawConn) error {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}
if sockErr != nil {
log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
logf("[unexpected] netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
}
return sockErr
}

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