Compare commits

...

90 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
121 changed files with 5829 additions and 1259 deletions

1
.gitattributes vendored
View File

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

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:"

View File

@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v2.2.4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts

View File

@@ -30,3 +30,11 @@ 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

@@ -30,6 +30,6 @@ go run github.com/tailscale/mkctr@latest \
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--tags="${VERSION_SHORT},${VERSION_MINOR}" \
--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, safesocket.WindowsLocalPort)
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
@@ -228,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")
}
@@ -295,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")
@@ -250,24 +252,26 @@ func main() {
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)
})
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)
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, safesocket.WindowsLocalPort)
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

@@ -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

@@ -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,7 +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/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
@@ -91,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

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+
@@ -62,6 +63,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
@@ -179,7 +181,7 @@ 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+
@@ -193,16 +195,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
@@ -250,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
@@ -273,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+
@@ -364,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

@@ -28,13 +28,16 @@ import (
"syscall"
"time"
"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"
@@ -175,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)
}
}
@@ -298,43 +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")
}
ns, err := newNetstack(logf, e)
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 {
log.Fatalf("failed to start netstack: %v", err)
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))
}()
}
}
@@ -364,13 +378,11 @@ func run() error {
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
logf("ipnserver.StateStore: %v", err)
return err
return fmt.Errorf("ipnserver.StateStore: %w", err)
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
if err != nil {
logf("ipnserver.New: %v", err)
return err
return fmt.Errorf("ipnserver.New: %w", err)
}
if debugMux != nil {
@@ -385,21 +397,20 @@ func run() error {
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
}
@@ -431,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"
@@ -508,26 +520,54 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn)
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,6 +32,7 @@ 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"
@@ -39,6 +40,7 @@ import (
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -78,7 +80,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
// 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.
logger := log.New(os.Stderr, "", 0)
// 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)
}()
@@ -114,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)
@@ -177,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 {
@@ -197,17 +211,19 @@ 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)
}
ns, err := newNetstack(logf, eng)
ns, err := newNetstack(logf, dialer, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
@@ -287,7 +303,7 @@ func startIPNServer(ctx context.Context, logid string) error {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
if err != nil {
logf("ipnserver.Run: %v", err)
}

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

@@ -139,6 +139,9 @@ func TestNoReuse(t *testing.T) {
t.Fatalf("server wire traffic seen twice")
}
packets[serverWire] = true
server.Close()
client.Close()
}
}

245
go.mod
View File

@@ -7,208 +7,257 @@ require (
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.14.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
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.1-0.20211101163509-b10eb8fe5cf6
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
golang.org/x/net v0.0.0-20211111083644-e5c967477495
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-20211110154304-99a53858aa08
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.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.2
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
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-20200728191858-db3c7e526aae // 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
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
gopkg.in/ini.v1 v1.62.0 // 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
)

1088
go.sum

File diff suppressed because it is too large Load Diff

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,7 +22,6 @@ import (
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"inet.af/netaddr"
@@ -36,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"
@@ -88,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()
@@ -141,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().
@@ -155,9 +160,14 @@ 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)
@@ -176,11 +186,13 @@ 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{}))
@@ -210,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.
@@ -221,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 {
@@ -389,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())
}
}
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,
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,
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,
})
}
}
@@ -600,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() {
@@ -1259,7 +1289,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
return
}
if apiSrv != nil && apiSrv.hasFilesWaiting() {
if apiSrv.hasFilesWaiting() {
n.FilesWaiting = &empty.Message{}
}
@@ -1668,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
@@ -1676,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()
@@ -1753,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
}
@@ -1862,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)
@@ -1960,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)
@@ -1987,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
@@ -2022,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
@@ -2071,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",
@@ -2079,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
@@ -2142,22 +2199,15 @@ 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 {
@@ -2640,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
@@ -2744,9 +2795,6 @@ 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()
}
@@ -2754,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)
}
@@ -2764,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)
}
@@ -2880,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
}
}
@@ -3009,19 +3051,6 @@ func disabledSysctls(sysctls ...string) (disabled []string, err error) {
return disabled, 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)
}
return nil
}
// DERPMap returns the current DERPMap in use, or nil if not connected.
func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
b.mu.Lock()
@@ -3053,3 +3082,55 @@ func (b *LocalBackend) OfferingExitNode() bool {
}
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)
}

View File

@@ -46,10 +46,13 @@ import (
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
@@ -57,10 +60,17 @@ type peerAPIServer struct {
// 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 (
@@ -77,6 +87,10 @@ const (
deletedSuffix = ".deleted"
)
func (s *peerAPIServer) canReceiveFiles() bool {
return s != nil && s.rootDir != ""
}
func validFilenameRune(r rune) bool {
switch r {
case '/':
@@ -123,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() {
@@ -183,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
@@ -248,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
@@ -263,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")
@@ -329,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")
@@ -363,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" {
@@ -468,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})
}
}
@@ -608,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()
@@ -680,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()
}
@@ -832,7 +863,7 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
defer cancel()
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr)
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 {
@@ -849,7 +880,7 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
return
}
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Content-Length", strconv.Itoa(len(q)))
w.Header().Set("Content-Length", strconv.Itoa(len(res)))
w.Write(res)
}
@@ -918,14 +949,19 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
j, _ := json.Marshal(struct {
Error string
}{err.Error()})
j = append(j, '\n')
w.Write(j)
return
}
}()
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
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
}

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

@@ -179,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"),
),
},
{

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)
}
@@ -941,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,6 +48,7 @@ import (
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
)
// Options is the configuration of the Tailscale node agent.
@@ -651,7 +652,7 @@ func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
//
// Deprecated: use New and Server.Run instead.
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
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)
@@ -735,7 +736,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
}
}
server, err := New(logf, logid, store, eng, serverModeUser, opts)
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
if err != nil {
return err
}
@@ -748,8 +749,8 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
// New returns a new Server.
//
// To start it, use the Server.Run method.
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)
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)
}
@@ -758,6 +759,22 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
return smallzstd.NewDecoder(nil)
})
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 {
@@ -851,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
@@ -1112,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)
}
@@ -72,6 +74,6 @@ func TestRunMultipleAccepts(t *testing.T) {
}
defer ln.Close()
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
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

@@ -88,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
@@ -242,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
}
@@ -275,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
}
@@ -290,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,7 +26,6 @@ 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"
@@ -376,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)
@@ -423,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)
}
@@ -457,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

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

View File

@@ -525,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,

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

@@ -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"
@@ -132,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.
//
@@ -394,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

@@ -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

@@ -13,6 +13,7 @@ import (
"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"
@@ -39,11 +40,14 @@ type Manager struct {
}
// 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)
@@ -230,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

@@ -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

@@ -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)),
}
@@ -423,8 +426,7 @@ 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://") {
metricDNSFwdErrorType.Add(1)
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)
@@ -580,9 +582,9 @@ func (f *forwarder) forward(query packet) error {
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
//
// If backupResolvers are specified, they're used in the case that no
// upstreams are available.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, backupResolvers ...resolverAndDelay) error {
// 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 {
@@ -601,13 +603,12 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
clampEDNSSize(query.bs, maxResponseBytes)
resolvers := f.resolvers(domain)
if len(resolvers) == 0 {
resolvers = backupResolvers
}
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
resolvers = f.resolvers(domain)
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
}
}
fq := &forwardQuery{

View File

@@ -8,11 +8,14 @@ package resolver
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"sort"
"strings"
@@ -20,12 +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"
)
@@ -186,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
@@ -217,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,
@@ -226,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
}
@@ -303,48 +315,84 @@ 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.
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort) (res []byte, err error) {
metricDNSQueryForPeer.Add(1)
//
// 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)
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
if err == errNoUpstreams {
// Handle to the system resolver.
switch runtime.GOOS {
case "linux":
// Assume for now that we don't have an upstream because
// they're using systemd-resolved and we're in Split DNS mode
// where we don't know the base config.
//
// TODO(bradfitz): this is a lazy assumption. Do better, and
// maybe move the HandleExitNodeDNSQuery method to the dns.Manager
// instead? But this works for now.
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolverAndDelay{
name: dnstype.Resolver{
Addr: "127.0.0.1:53",
},
})
default:
// TODO(bradfitz): if we're on an exit node
// on, say, Windows, we need to parse the DNS
// packet in q and call OS-native APIs for
// each question. But we'll want to strip out
// questions for MagicDNS names probably, so
// they don't loop back into
// 100.100.100.100. We don't want to resolve
// MagicDNS names across Tailnets once we
// permit sharing exit nodes.
//
// For now, just return an error.
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
}
}
if err != nil {
metricDNSQueryForPeerError.Add(1)
return nil, err
}
select {
case p, ok := <-ch:
@@ -357,6 +405,159 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
}
}
// 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).
@@ -504,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{
@@ -538,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 {
@@ -582,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 {
@@ -601,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
@@ -611,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 {
@@ -637,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
@@ -825,6 +1150,37 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
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")
@@ -837,8 +1193,10 @@ var (
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
metricDNSQueryForPeer = clientmetric.NewCounter("dns_query_peerapi")
metricDNSQueryForPeerError = clientmetric.NewCounter("dns_query_peerapi_error")
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")

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

@@ -76,7 +76,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
if debugPipe {
orig := b
defer func() {
log.Printf("Pipe(%q).Read( %q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
log.Printf("Pipe(%q).Read(%q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
}()
}
for n == 0 {

View File

@@ -60,9 +60,6 @@ func TestPipeTimeout(t *testing.T) {
t.Run("block-write", func(t *testing.T) {
p := NewPipe("p1", 1<<16)
p.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
if _, err := p.Write([]byte{'h'}); err != nil {
t.Fatal(err)
}
if err := p.Block(); err != nil {
t.Fatal(err)
}
@@ -75,9 +72,6 @@ func TestPipeTimeout(t *testing.T) {
p.Write([]byte{'h', 'i'})
p.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
b := make([]byte, 1)
if _, err := p.Read(b); err != nil {
t.Fatal(err)
}
if err := p.Block(); err != nil {
t.Fatal(err)
}

View File

@@ -39,6 +39,16 @@ type Header interface {
Marshal(buf []byte) error
}
// HeaderChecksummer is implemented by Header implementations that
// need to do a checksum over their paylods.
type HeaderChecksummer interface {
Header
// WriteCheck writes the correct checksum into buf, which should
// be be the already-marshalled header and payload.
WriteChecksum(buf []byte)
}
// Generate generates a new packet with the given Header and
// payload. This function allocates memory, see Header.Marshal for an
// allocation-free option.
@@ -49,5 +59,9 @@ func Generate(h Header, payload []byte) []byte {
copy(buf[hlen:], payload)
h.Marshal(buf)
if hc, ok := h.(HeaderChecksummer); ok {
hc.WriteChecksum(buf)
}
return buf
}

View File

@@ -4,6 +4,12 @@
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
@@ -42,3 +48,120 @@ type ICMP6Code uint8
const (
ICMP6NoCode ICMP6Code = 0
)
// ICMP6Header is an IPv4+ICMPv4 header.
type ICMP6Header struct {
IP6Header
Type ICMP6Type
Code ICMP6Code
}
// Len implements Header.
func (h ICMP6Header) Len() int {
return h.IP6Header.Len() + icmp6HeaderLength
}
// Marshal implements Header.
func (h ICMP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.ICMPv6
h.IP6Header.Marshal(buf)
const o = ip6HeaderLength // start offset of ICMPv6 header
buf[o+0] = uint8(h.Type)
buf[o+1] = uint8(h.Code)
buf[o+2] = 0 // checksum, to be filled in later
buf[o+3] = 0 // checksum, to be filled in later
return nil
}
// ToResponse implements Header. TODO: it doesn't implement it
// correctly, instead it statically generates an ICMP Echo Reply
// packet.
func (h *ICMP6Header) ToResponse() {
// TODO: this doesn't implement ToResponse correctly, as it
// assumes the ICMP request type.
h.Type = ICMP6EchoReply
h.Code = ICMP6NoCode
h.IP6Header.ToResponse()
}
// WriteChecksum implements HeaderChecksummer, writing just the checksum bytes
// into the otherwise fully marshaled ICMP6 packet p (which should include the
// IPv6 header, ICMPv6 header, and payload).
func (h ICMP6Header) WriteChecksum(p []byte) {
const payOff = ip6HeaderLength + icmp6HeaderLength
xsum := icmp6Checksum(p[ip6HeaderLength:payOff], h.Src.As16(), h.Dst.As16(), p[payOff:])
binary.BigEndian.PutUint16(p[ip6HeaderLength+2:], xsum)
}
// Adapted from gVisor:
// icmp6Checksum calculates the ICMP checksum over the provided ICMPv6
// header (without the IPv6 header), IPv6 src/dst addresses and the
// payload.
//
// The header's existing checksum must be zeroed.
func icmp6Checksum(header []byte, src, dst [16]byte, payload []byte) uint16 {
// Calculate the IPv6 pseudo-header upper-layer checksum.
xsum := checksumBytes(src[:], 0)
xsum = checksumBytes(dst[:], xsum)
var scratch [4]byte
binary.BigEndian.PutUint32(scratch[:], uint32(len(header)+len(payload)))
xsum = checksumBytes(scratch[:], xsum)
xsum = checksumBytes(append(scratch[:0], 0, 0, 0, uint8(ipproto.ICMPv6)), xsum)
xsum = checksumBytes(payload, xsum)
var hdrz [icmp6HeaderLength]byte
copy(hdrz[:], header)
// Zero out the header.
hdrz[2] = 0
hdrz[3] = 0
xsum = ^checksumBytes(hdrz[:], xsum)
return xsum
}
// checksumCombine combines the two uint16 to form their
// checksum. This is done by adding them and the carry.
//
// Note that checksum a must have been computed on an even number of
// bytes.
func checksumCombine(a, b uint16) uint16 {
v := uint32(a) + uint32(b)
return uint16(v + v>>16)
}
// checksumBytes calculates the checksum (as defined in RFC 1071) of
// the bytes in buf.
//
// The initial checksum must have been computed on an even number of bytes.
func checksumBytes(buf []byte, initial uint16) uint16 {
v := uint32(initial)
odd := len(buf)%2 == 1
if odd {
v += uint32(buf[0])
buf = buf[1:]
}
n := len(buf)
odd = n&1 != 0
if odd {
n--
v += uint32(buf[n]) << 8
}
for i := 0; i < n; i += 2 {
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
}
return checksumCombine(uint16(v), uint16(v>>16))
}

80
net/packet/icmp6_test.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package packet
import (
"testing"
"inet.af/netaddr"
"tailscale.com/types/ipproto"
)
func TestICMPv6PingResponse(t *testing.T) {
pingHdr := ICMP6Header{
IP6Header: IP6Header{
Src: netaddr.MustParseIP("1::1"),
Dst: netaddr.MustParseIP("2::2"),
IPProto: ipproto.ICMPv6,
},
Type: ICMP6EchoRequest,
Code: ICMP6NoCode,
}
// echoReqLen is 2 bytes identifier + 2 bytes seq number.
// https://datatracker.ietf.org/doc/html/rfc4443#section-4.1
// Packet.IsEchoRequest verifies that these 4 bytes are present.
const echoReqLen = 4
buf := make([]byte, pingHdr.Len()+echoReqLen)
if err := pingHdr.Marshal(buf); err != nil {
t.Fatal(err)
}
var p Parsed
p.Decode(buf)
if !p.IsEchoRequest() {
t.Fatalf("not an echo request, got: %+v", p)
}
pingHdr.ToResponse()
buf = make([]byte, pingHdr.Len()+echoReqLen)
if err := pingHdr.Marshal(buf); err != nil {
t.Fatal(err)
}
p.Decode(buf)
if p.IsEchoRequest() {
t.Fatalf("unexpectedly still an echo request: %+v", p)
}
if !p.IsEchoResponse() {
t.Fatalf("not an echo response: %+v", p)
}
}
func TestICMPv6Checksum(t *testing.T) {
const req = "\x60\x0f\x07\x00\x00\x10\x3a\x40\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
"\x00\x00\x00\x00\x00\x00\x20\x0e\x80\x00\x4a\x9a\x2e\xea\x00\x02" +
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
// The packet that we'd originally generated incorrectly, but with the checksum
// bytes fixed per WireShark's correct calculation:
const wantRes = "\x60\x00\xf8\xff\x00\x10\x3a\x40\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
"\x00\x00\x00\x00\x00\x00\x20\x0e\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x81\x00\x49\x9a\x2e\xea\x00\x02" +
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
var p Parsed
p.Decode([]byte(req))
if !p.IsEchoRequest() {
t.Fatalf("not an echo request, got: %+v", p)
}
h := p.ICMP6Header()
h.ToResponse()
pong := Generate(&h, p.Payload())
if string(pong) != wantRes {
t.Errorf("wrong packet\n\n got: %x\nwant: %x", pong, wantRes)
}
}

View File

@@ -57,7 +57,7 @@ func (h *IP6Header) ToResponse() {
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums.
func (h IP6Header) marshalPseudo(buf []byte) error {
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
@@ -72,6 +72,6 @@ func (h IP6Header) marshalPseudo(buf []byte) error {
buf[36] = 0
buf[37] = 0
buf[38] = 0
buf[39] = 17 // NextProto
buf[39] = byte(proto) // NextProto
return nil
}

View File

@@ -75,7 +75,7 @@ func (p *Parsed) String() string {
}
// Decode extracts data from the packet in b into q.
// It performs extremely simple packet decoding for basic IPv4 packet types.
// It performs extremely simple packet decoding for basic IPv4 and IPv6 packet types.
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
// and shouldn't need any memory allocation.
func (q *Parsed) Decode(b []byte) {
@@ -339,9 +339,6 @@ func (q *Parsed) IP6Header() IP6Header {
}
func (q *Parsed) ICMP4Header() ICMP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
return ICMP4Header{
IP4Header: q.IP4Header(),
Type: ICMP4Type(q.b[q.subofs+0]),
@@ -349,10 +346,15 @@ func (q *Parsed) ICMP4Header() ICMP4Header {
}
}
func (q *Parsed) UDP4Header() UDP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
func (q *Parsed) ICMP6Header() ICMP6Header {
return ICMP6Header{
IP6Header: q.IP6Header(),
Type: ICMP6Type(q.b[q.subofs+0]),
Code: ICMP6Code(q.b[q.subofs+1]),
}
}
func (q *Parsed) UDP4Header() UDP4Header {
return UDP4Header{
IP4Header: q.IP4Header(),
SrcPort: q.Src.Port(),
@@ -410,7 +412,7 @@ func (q *Parsed) IsEchoRequest() bool {
}
}
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
func (q *Parsed) IsEchoResponse() bool {
switch q.IPProto {
case ipproto.ICMPv4:

View File

@@ -40,7 +40,7 @@ func (h UDP6Header) Marshal(buf []byte) error {
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP6Header.marshalPseudo(buf)
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
h.IP6Header.Marshal(buf)

View File

@@ -63,11 +63,18 @@ type igdCounters struct {
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
d := &TestIGD{
logf: logf,
doPMP: t.PMP,
doPCP: t.PCP,
doUPnP: t.UPnP,
}
d.logf = func(msg string, args ...interface{}) {
// Don't log after the device has closed;
// stray trailing logging angers testing.T.Logf.
if d.closed.Get() {
return
}
logf(msg, args...)
}
var err error
if d.upnpConn, err = testListenUDP(); err != nil {
return nil, err

View File

@@ -19,6 +19,10 @@ import (
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
type pcpResultCode uint8
// PCP constants
const (
pcpVersion = 2
@@ -26,8 +30,14 @@ const (
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
pcpCodeOK = 0
pcpCodeNotAuthorized = 2
pcpCodeOK pcpResultCode = 0
pcpCodeNotAuthorized pcpResultCode = 2
// From RFC 6887:
// ADDRESS_MISMATCH: The source IP address of the request packet does
// not match the contents of the PCP Client's IP Address field, due
// to an unexpected NAT on the path between the PCP client and the
// PCP-controlled NAT or firewall.
pcpCodeAddressMismatch pcpResultCode = 12
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
@@ -140,7 +150,7 @@ func pcpAnnounceRequest(myIP netaddr.IP) []byte {
type pcpResponse struct {
OpCode uint8
ResultCode uint8
ResultCode pcpResultCode
Lifetime uint32
Epoch uint32
}
@@ -150,7 +160,7 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
return
}
res.OpCode = b[1]
res.ResultCode = b[3]
res.ResultCode = pcpResultCode(b[3])
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pcpCodeOK-0]
_ = x[pcpCodeNotAuthorized-2]
_ = x[pcpCodeAddressMismatch-12]
}
const (
_pcpResultCode_name_0 = "OK"
_pcpResultCode_name_1 = "NotAuthorized"
_pcpResultCode_name_2 = "AddressMismatch"
)
func (i pcpResultCode) String() string {
switch {
case i == 0:
return _pcpResultCode_name_0
case i == 2:
return _pcpResultCode_name_1
case i == 12:
return _pcpResultCode_name_2
default:
return "pcpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pmpCodeOK-0]
_ = x[pmpCodeUnsupportedVersion-1]
_ = x[pmpCodeNotAuthorized-2]
_ = x[pmpCodeNetworkFailure-3]
_ = x[pmpCodeOutOfResources-4]
_ = x[pmpCodeUnsupportedOpcode-5]
}
const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode"
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
func (i pmpResultCode) String() string {
if i >= pmpResultCode(len(_pmpResultCode_index)-1) {
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _pmpResultCode_name[_pmpResultCode_index[i]:_pmpResultCode_index[i+1]]
}

View File

@@ -22,6 +22,7 @@ import (
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
)
// Debug knobs for "tailscaled debug --portmap".
@@ -563,6 +564,8 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
}
}
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
type pmpResultCode uint16
// NAT-PMP constants.
@@ -685,11 +688,13 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
if c.sawPMPRecently() {
res.PMP = true
} else if !DisablePMP {
metricPMPSent.Add(1)
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else if !DisablePCP {
metricPCPSent.Add(1)
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
}
if c.sawUPnPRecently() {
@@ -734,6 +739,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
// See https://github.com/tailscale/tailscale/issues/3197 for
// an example of a device that strictly implements UPnP, and
// only responds to multicast queries.
metricUPnPSent.Add(1)
uc.WriteTo(uPnPPacket, upnpAddr)
uc.WriteTo(uPnPPacket, upnpMulticastAddr)
}
@@ -759,11 +765,15 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
port := uint16(addr.(*net.UDPAddr).Port)
switch port {
case c.upnpPort():
metricUPnPResponse.Add(1)
if ip == gw && mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
meta, err := parseUPnPDiscoResponse(buf[:n])
if err != nil {
c.logf("unrecognized UPnP discovery response; ignoring")
metricUPnPParseErr.Add(1)
c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
continue
}
metricUPnPOK.Add(1)
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
res.UPnP = true
c.mu.Lock()
@@ -771,10 +781,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
if c.uPnPMeta != meta {
c.logf("UPnP meta changed: %+v", meta)
c.uPnPMeta = meta
metricUPnPUpdatedMeta.Add(1)
}
c.mu.Unlock()
}
case c.pxpPort(): // same value for PMP and PCP
metricPXPResponse.Add(1)
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
pcpHeard = true
@@ -785,25 +797,35 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
case pcpCodeOK:
c.logf("[v1] Got PCP response: epoch: %v", pres.Epoch)
res.PCP = true
metricPCPOK.Add(1)
continue
case pcpCodeNotAuthorized:
// A PCP service is running, but refuses to
// provide port mapping services.
res.PCP = false
metricPCPNotAuthorized.Add(1)
continue
case pcpCodeAddressMismatch:
// A PCP service is running, but it is behind a NAT, so it can't help us.
res.PCP = false
metricPCPAddressMismatch.Add(1)
continue
default:
// Fall through to unexpected log line.
}
}
metricPCPUnhandledResponseCode.Add(1)
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode != pmpOpReply|pmpOpMapPublicAddr {
c.logf("unexpected PMP probe response opcode: %+v", pres)
metricPMPUnhandledOpcode.Add(1)
continue
}
switch pres.ResultCode {
case pmpCodeOK:
metricPMPOK.Add(1)
c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
@@ -812,11 +834,20 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
case pmpCodeNotAuthorized, pmpCodeNetworkFailure, pmpCodeOutOfResources:
// Normal failures.
case pmpCodeNotAuthorized:
metricPMPNotAuthorized.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeNetworkFailure:
metricPMPNetworkFailure.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeOutOfResources:
metricPMPOutOfResources.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
}
metricPMPUnhandledResponseCode.Add(1)
c.logf("unexpected PMP probe response: %+v", pres)
}
}
@@ -835,3 +866,74 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
// PCP/PMP metrics
var (
// metricPXPResponse counts the number of times we received a PMP/PCP response.
metricPXPResponse = clientmetric.NewCounter("portmap_pxp_response")
// metricPCPSent counts the number of times we sent a PCP request.
metricPCPSent = clientmetric.NewCounter("portmap_pcp_sent")
// metricPCPOK counts the number of times
// we received a successful PCP response.
metricPCPOK = clientmetric.NewCounter("portmap_pcp_ok")
// metricPCPAddressMismatch counts the number of times
// we received a PCP address mismatch result code.
metricPCPAddressMismatch = clientmetric.NewCounter("portmap_pcp_address_mismatch")
// metricPCPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPCPNotAuthorized = clientmetric.NewCounter("portmap_pcp_not_authorized")
// metricPCPUnhandledResponseCode counts the number of times
// we received an (as yet) unhandled PCP result code.
metricPCPUnhandledResponseCode = clientmetric.NewCounter("portmap_pcp_unhandled_response_code")
// metricPMPSent counts the number of times we sent a PMP request.
metricPMPSent = clientmetric.NewCounter("portmap_pmp_sent")
// metricPMPOK counts the number of times
// we received a succesful PMP response.
metricPMPOK = clientmetric.NewCounter("portmap_pmp_ok")
// metricPMPUnhandledOpcode counts the number of times
// we received an unhandled PMP opcode.
metricPMPUnhandledOpcode = clientmetric.NewCounter("portmap_pmp_unhandled_opcode")
// metricPMPUnhandledResponseCode counts the number of times
// we received an unhandled PMP result code.
metricPMPUnhandledResponseCode = clientmetric.NewCounter("portmap_pmp_unhandled_response_code")
// metricPMPOutOfResources counts the number of times
// we received a PCP out of resources result code.
metricPMPOutOfResources = clientmetric.NewCounter("portmap_pmp_out_of_resources")
// metricPMPNetworkFailure counts the number of times
// we received a PCP network failure result code.
metricPMPNetworkFailure = clientmetric.NewCounter("portmap_pmp_network_failure")
// metricPMPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPMPNotAuthorized = clientmetric.NewCounter("portmap_pmp_not_authorized")
)
// UPnP metrics
var (
// metricUPnPSent counts the number of times we sent a UPnP request.
metricUPnPSent = clientmetric.NewCounter("portmap_upnp_sent")
// metricUPnPResponse counts the number of times we received a UPnP response.
metricUPnPResponse = clientmetric.NewCounter("portmap_upnp_response")
// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")
// metricUPnPOK counts the number of times we received a usable UPnP response.
metricUPnPOK = clientmetric.NewCounter("portmap_upnp_ok")
// metricUPnPUpdatedMeta counts the number of times
// we received a UPnP response with a new meta.
metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
)

145
net/proxymux/mux.go Normal file
View File

@@ -0,0 +1,145 @@
// 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 proxymux splits a net.Listener in two, routing SOCKS5
// connections to one and HTTP requests to the other.
//
// It allows for hosting both a SOCKS5 proxy and an HTTP proxy on the
// same listener.
package proxymux
import (
"io"
"net"
"sync"
"time"
)
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
// through to either socksListener or httpListener, depending the
// first byte sent by the client.
func SplitSOCKSAndHTTP(ln net.Listener) (socksListener, httpListener net.Listener) {
sl := &listener{
addr: ln.Addr(),
c: make(chan net.Conn),
closed: make(chan struct{}),
}
hl := &listener{
addr: ln.Addr(),
c: make(chan net.Conn),
closed: make(chan struct{}),
}
go splitSOCKSAndHTTPListener(ln, sl, hl)
return sl, hl
}
func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) {
for {
conn, err := ln.Accept()
if err != nil {
sl.Close()
hl.Close()
return
}
go routeConn(conn, sl, hl)
}
}
func routeConn(c net.Conn, socksListener, httpListener *listener) {
if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
c.Close()
return
}
var b [1]byte
if _, err := io.ReadFull(c, b[:]); err != nil {
c.Close()
return
}
if err := c.SetReadDeadline(time.Time{}); err != nil {
c.Close()
return
}
conn := &connWithOneByte{
Conn: c,
b: b[0],
}
// First byte of a SOCKS5 session is a version byte set to 5.
var ln *listener
if b[0] == 5 {
ln = socksListener
} else {
ln = httpListener
}
select {
case ln.c <- conn:
case <-ln.closed:
c.Close()
}
}
type listener struct {
addr net.Addr
c chan net.Conn
mu sync.Mutex // serializes close() on closed. It's okay to receive on closed without locking.
closed chan struct{}
}
func (ln *listener) Accept() (net.Conn, error) {
// Once closed, reliably stay closed, don't race with attempts at
// further connections.
select {
case <-ln.closed:
return nil, net.ErrClosed
default:
}
select {
case ret := <-ln.c:
return ret, nil
case <-ln.closed:
return nil, net.ErrClosed
}
}
func (ln *listener) Close() error {
ln.mu.Lock()
defer ln.mu.Unlock()
select {
case <-ln.closed:
// Already closed
default:
close(ln.closed)
}
return nil
}
func (ln *listener) Addr() net.Addr {
return ln.addr
}
// connWithOneByte is a net.Conn that returns b for the first read
// request, then forwards everything else to Conn.
type connWithOneByte struct {
net.Conn
b byte
bRead bool
}
func (c *connWithOneByte) Read(bs []byte) (int, error) {
if c.bRead {
return c.Conn.Read(bs)
}
if len(bs) == 0 {
return 0, nil
}
c.bRead = true
bs[0] = c.b
return 1, nil
}

172
net/proxymux/mux_test.go Normal file
View File

@@ -0,0 +1,172 @@
// 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 proxymux
import (
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"testing"
"tailscale.com/net/socks5"
)
func TestSplitSOCKSAndHTTP(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.checkURL(s.httpClient, false)
s.checkURL(s.socksClient, false)
}
func TestSplitSOCKSAndHTTPCloseSocks(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.socksListener.Close()
s.checkURL(s.httpClient, false)
s.checkURL(s.socksClient, true)
}
func TestSplitSOCKSAndHTTPCloseHTTP(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.httpListener.Close()
s.checkURL(s.httpClient, true)
s.checkURL(s.socksClient, false)
}
func TestSplitSOCKSAndHTTPCloseBoth(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.httpListener.Close()
s.socksListener.Close()
s.checkURL(s.httpClient, true)
s.checkURL(s.socksClient, true)
}
type world struct {
t *testing.T
// targetListener/target is the HTTP server the client wants to
// reach. It unconditionally responds with HTTP 418 "I'm a
// teapot".
targetListener net.Listener
target http.Server
targetURL string
// httpListener/httpProxy is an HTTP proxy that can proxy to
// target.
httpListener net.Listener
httpProxy http.Server
// socksListener/socksProxy is a SOCKS5 proxy that can dial
// targetListener.
socksListener net.Listener
socksProxy *socks5.Server
// jointListener is the mux that serves both HTTP and SOCKS5
// proxying.
jointListener net.Listener
// httpClient and socksClient are HTTP clients configured to proxy
// through httpProxy and socksProxy respectively.
httpClient *http.Client
socksClient *http.Client
}
func (s *world) checkURL(c *http.Client, wantErr bool) {
s.t.Helper()
resp, err := c.Get(s.targetURL)
if wantErr {
if err == nil {
s.t.Errorf("HTTP request succeeded unexpectedly: got HTTP code %d, wanted failure", resp.StatusCode)
}
} else if err != nil {
s.t.Errorf("HTTP request failed: %v", err)
} else if c := resp.StatusCode; c != http.StatusTeapot {
s.t.Errorf("unexpected status code: got %d, want %d", c, http.StatusTeapot)
}
}
func (s *world) Close() {
s.jointListener.Close()
s.socksListener.Close()
s.httpProxy.Close()
s.httpListener.Close()
s.target.Close()
s.targetListener.Close()
}
func mkWorld(t *testing.T) (ret *world) {
t.Helper()
ret = &world{
t: t,
}
var err error
ret.targetListener, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
ret.target = http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}),
}
go ret.target.Serve(ret.targetListener)
ret.targetURL = fmt.Sprintf("http://%s/", ret.targetListener.Addr().String())
ret.jointListener, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
ret.socksListener, ret.httpListener = SplitSOCKSAndHTTP(ret.jointListener)
httpProxy := http.Server{
Handler: httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: ret.targetListener.Addr().String(),
Path: "/",
}),
}
go httpProxy.Serve(ret.httpListener)
socksProxy := socks5.Server{}
go socksProxy.Serve(ret.socksListener)
ret.httpClient = &http.Client{
Transport: &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) {
return &url.URL{
Scheme: "http",
Host: ret.jointListener.Addr().String(),
Path: "/",
}, nil
},
DisableKeepAlives: true, // one connection per request
},
}
ret.socksClient = &http.Client{
Transport: &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) {
return &url.URL{
Scheme: "socks5",
Host: ret.jointListener.Addr().String(),
Path: "/",
}, nil
},
DisableKeepAlives: true, // one connection per request
},
}
return ret
}

View File

@@ -1,79 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tssocks is the glue between Tailscale and the net/socks5 package.
package tssocks
import (
"context"
"net"
"sync"
"inet.af/netaddr"
"tailscale.com/net/socks5"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
)
// NewServer returns a new SOCKS5 server configured to dial out to
// Tailscale addresses.
//
// The returned server is not yet listening. The caller must call
// Serve with a listener.
//
// If ns is non-nil, it is used for dialing when needed.
func NewServer(logf logger.Logf, e wgengine.Engine, ns *netstack.Impl) *socks5.Server {
d := &dialer{ns: ns}
e.AddNetworkMapCallback(d.onNewNetmap)
return &socks5.Server{
Logf: logf,
Dialer: d.DialContext,
}
}
// dialer is the Tailscale SOCKS5 dialer.
type dialer struct {
ns *netstack.Impl
mu sync.Mutex
dns netstack.DNSMap
}
func (d *dialer) onNewNetmap(nm *netmap.NetworkMap) {
d.mu.Lock()
defer d.mu.Unlock()
d.dns = netstack.DNSMapFromNetworkMap(nm)
}
func (d *dialer) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
d.mu.Unlock()
return dns.Resolve(ctx, addr)
}
func (d *dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.resolve(ctx, addr)
if err != nil {
return nil, err
}
if d.ns != nil && d.useNetstackForIP(ipp.IP()) {
return d.ns.DialContextTCP(ctx, ipp.String())
}
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}
func (d *dialer) useNetstackForIP(ip netaddr.IP) bool {
if d.ns == nil {
return false
}
// TODO(bradfitz): this isn't exactly right.
// We should also support subnets when the
// prefs are configured as such.
return tsaddr.IsTailscaleIP(ip)
}

115
net/tsdial/dnsmap.go Normal file
View File

@@ -0,0 +1,115 @@
// 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 tsdial
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"inet.af/netaddr"
"tailscale.com/types/netmap"
"tailscale.com/util/dnsname"
)
// dnsMap maps MagicDNS names (both base + FQDN) to their first IP.
// It must not be mutated once created.
//
// Example keys are "foo.domain.tld.beta.tailscale.net" and "foo",
// both without trailing dots.
type dnsMap map[string]netaddr.IP
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if nm == nil {
return nil
}
ret := make(dnsMap)
suffix := nm.MagicDNSSuffix()
have4 := false
if nm.Name != "" && len(nm.Addresses) > 0 {
ip := nm.Addresses[0].IP()
ret[strings.TrimRight(nm.Name, ".")] = ip
if dnsname.HasSuffix(nm.Name, suffix) {
ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip
}
for _, a := range nm.Addresses {
if a.IP().Is4() {
have4 = true
}
}
}
for _, p := range nm.Peers {
if p.Name == "" {
continue
}
for _, a := range p.Addresses {
ip := a.IP()
if ip.Is4() && !have4 {
continue
}
ret[strings.TrimRight(p.Name, ".")] = ip
if dnsname.HasSuffix(p.Name, suffix) {
ret[dnsname.TrimSuffix(p.Name, suffix)] = ip
}
break
}
}
for _, rec := range nm.DNS.ExtraRecords {
if rec.Type != "" {
continue
}
ip, err := netaddr.ParseIP(rec.Value)
if err != nil {
continue
}
ret[strings.TrimRight(rec.Name, ".")] = ip
}
return ret
}
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
var errUnresolved = errors.New("address well formed but not resolved")
func splitHostPort(addr string) (host string, port uint16, err error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return "", 0, err
}
port16, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return "", 0, fmt.Errorf("invalid port in address %q", addr)
}
return host, uint16(port16), nil
}
// Resolve resolves addr into an IP:port using first the MagicDNS contents
// of m, else using the system resolver.
//
// The error is [exactly] errUnresolved if the addr is a name that isn't known
// in the map.
func (m dnsMap) resolveMemory(ctx context.Context, network, addr string) (_ netaddr.IPPort, err error) {
host, port, err := splitHostPort(addr)
if err != nil {
// addr malformed or invalid port.
return netaddr.IPPort{}, err
}
if ip, err := netaddr.ParseIP(host); err == nil {
// addr was literal ip:port.
return netaddr.IPPortFrom(ip, port), nil
}
// Host is not an IP, so assume it's a DNS name.
// Try MagicDNS first, otherwise a real DNS lookup.
ip := m[host]
if !ip.IsZero() {
return netaddr.IPPortFrom(ip, port), nil
}
return netaddr.IPPort{}, errUnresolved
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package netstack
package tsdial
import (
"reflect"
@@ -19,7 +19,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
want DNSMap
want dnsMap
}{
{
name: "self",
@@ -30,7 +30,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
pfx("100::123/128"),
},
},
want: DNSMap{
want: dnsMap{
"foo": ip("100.102.103.104"),
"foo.tailnet": ip("100.102.103.104"),
},
@@ -59,7 +59,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
},
},
},
want: DNSMap{
want: dnsMap{
"foo": ip("100.102.103.104"),
"foo.tailnet": ip("100.102.103.104"),
"a": ip("100.0.0.201"),
@@ -91,7 +91,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
},
},
},
want: DNSMap{
want: dnsMap{
"foo": ip("100::123"),
"foo.tailnet": ip("100::123"),
"a": ip("100::201"),
@@ -103,7 +103,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DNSMapFromNetworkMap(tt.nm)
got := dnsMapFromNetworkMap(tt.nm)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mismatch:\n got %v\nwant %v\n", got, tt.want)
}

101
net/tsdial/dohclient.go Normal file
View File

@@ -0,0 +1,101 @@
// 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 tsdial
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"tailscale.com/net/dnscache"
)
// dohConn is a net.PacketConn suitable for returning from
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
// ExitDNS DoH proxy service.
type dohConn struct {
ctx context.Context
baseURL string
hc *http.Client // if nil, default is used
dnsCache *dnscache.MessageCache
rbuf bytes.Buffer
}
var (
_ net.Conn = (*dohConn)(nil)
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
)
func (*dohConn) Close() error { return nil }
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
func (*dohConn) SetDeadline(t time.Time) error { return nil }
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.Write(p)
}
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = c.Read(p)
return n, todoAddr{}, err
}
func (c *dohConn) Read(p []byte) (n int, err error) {
return c.rbuf.Read(p)
}
func (c *dohConn) Write(packet []byte) (n int, err error) {
if c.dnsCache != nil {
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
if err == nil {
// Cache hit.
// TODO(bradfitz): add clientmetric
return len(packet), nil
}
c.rbuf.Reset()
}
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
if err != nil {
return 0, err
}
const dohType = "application/dns-message"
req.Header.Set("Content-Type", dohType)
hc := c.hc
if hc == nil {
hc = http.DefaultClient
}
hres, err := hc.Do(req)
if err != nil {
return 0, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
return 0, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
}
_, err = io.Copy(&c.rbuf, hres.Body)
if err != nil {
return 0, err
}
if c.dnsCache != nil {
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
}
return len(packet), nil
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdial
import (
"context"
"flag"
"net"
"testing"
"time"
)
var dohBase = flag.String("doh-base", "", "DoH base URL for manual DoH tests; e.g. \"http://100.68.82.120:47830/dns-query\"")
func TestDoHResolve(t *testing.T) {
if *dohBase == "" {
t.Skip("skipping manual test without --doh-base= set")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var r net.Resolver
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{ctx: ctx, baseURL: *dohBase}, nil
}
addrs, err := r.LookupIP(ctx, "ip4", "google.com.")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %q", addrs)
}

View File

@@ -0,0 +1,43 @@
// 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.
// This file's built on iOS and on two of three macOS build variants:
// the two GUI variants that both use Extensions (Network Extension
// and System Extension). It's not used on tailscaled-on-macOS.
//go:build ts_macext && (darwin || ios)
// +build ts_macext
// +build darwin ios
package tsdial
import (
"errors"
"net"
"syscall"
"tailscale.com/net/netns"
)
func init() {
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
func peerDialControlFuncNetworkExtension(d *Dialer) func(network, address string, c syscall.RawConn) error {
d.mu.Lock()
defer d.mu.Unlock()
index := -1
if x, ok := d.interfaceIndexLocked(d.tunName); ok {
index = x
}
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)
}
}

281
net/tsdial/tsdial.go Normal file
View File

@@ -0,0 +1,281 @@
// 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 tsdial provides a Dialer type that can dial out of tailscaled.
package tsdial
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"inet.af/netaddr"
"tailscale.com/net/dnscache"
"tailscale.com/net/netknob"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/monitor"
)
// Dialer dials out of tailscaled, while taking care of details while
// handling the dozens of edge cases depending on the server mode
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc.
type Dialer struct {
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
// it's non-nil) should be used to dial the provided IP.
UseNetstackForIP func(netaddr.IP) bool
// NetstackDialTCP dials the provided IPPort using netstack.
// If nil, it's not used.
NetstackDialTCP func(context.Context, netaddr.IPPort) (net.Conn, error)
peerDialControlFuncAtomic atomic.Value // of func() func(network, address string, c syscall.RawConn) error
peerClientOnce sync.Once
peerClient *http.Client
peerDialerOnce sync.Once
peerDialer *net.Dialer
mu sync.Mutex
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
// etc). This is needed on some platforms to set sockopts to bind
// to the same interface index.
func (d *Dialer) SetTUNName(name string) {
d.mu.Lock()
defer d.mu.Unlock()
d.tunName = name
}
// TUNName returns the name of the tun device in use, if any.
// Example format ("tailscale0", "utun6").
func (d *Dialer) TUNName() string {
d.mu.Lock()
defer d.mu.Unlock()
return d.tunName
}
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
// The doh URL should contain the scheme, authority, and path, but without
// a '?' and/or query parameters.
//
// For example, "http://100.68.82.120:47830/dns-query".
func (d *Dialer) SetExitDNSDoH(doh string) {
d.mu.Lock()
defer d.mu.Unlock()
if d.exitDNSDoHBase == doh {
return
}
d.exitDNSDoHBase = doh
if doh != "" && d.dnsCache == nil {
d.dnsCache = new(dnscache.MessageCache)
}
if d.dnsCache != nil {
d.dnsCache.Flush()
}
}
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
d.mu.Lock()
defer d.mu.Unlock()
d.linkMon = mon
}
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
if d.linkMon == nil {
return 0, false
}
st := d.linkMon.InterfaceState()
iface, ok := st.Interface[ifName]
if !ok {
return 0, false
}
return iface.Index, true
}
// peerDialControlFunc is non-nil on platforms that require a way to
// bind to dial out to other peers.
var peerDialControlFunc func(*Dialer) func(network, address string, c syscall.RawConn) error
// PeerDialControlFunc returns a function
// that can assigned to net.Dialer.Control to set sockopts or whatnot
// to make a dial escape the current platform's network sandbox.
//
// On many platforms the returned func will be nil.
//
// Notably, this is non-nil on iOS and macOS when run as a Network or
// System Extension (the GUI variants).
func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
if peerDialControlFunc == nil {
return nil
}
return peerDialControlFunc(d)
}
// SetNetMap sets the current network map and notably, the DNS names
// in its DNS configuration.
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
m := dnsMapFromNetworkMap(nm)
d.mu.Lock()
defer d.mu.Unlock()
d.dns = m
}
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
exitDNSDoH := d.exitDNSDoHBase
d.mu.Unlock()
// MagicDNS or otherwise baked in to the NetworkMap? Try that first.
ipp, err := dns.resolveMemory(ctx, network, addr)
if err != errUnresolved {
return ipp, err
}
// Otherwise, hit the network.
// TODO(bradfitz): wire up net/dnscache too.
host, port, err := splitHostPort(addr)
if err != nil {
// addr is malformed.
return netaddr.IPPort{}, err
}
var r net.Resolver
if exitDNSDoH != "" && runtime.GOOS != "windows" { // Windows: https://github.com/golang/go/issues/33097
r.PreferGo = true
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{
ctx: ctx,
baseURL: exitDNSDoH,
hc: d.PeerAPIHTTPClient(),
dnsCache: d.dnsCache,
}, nil
}
}
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
if err != nil {
return netaddr.IPPort{}, err
}
if len(ips) == 0 {
return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
}
ip, _ := netaddr.FromStdIP(ips[0])
return netaddr.IPPortFrom(ip, port), nil
}
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
// to the input value of "tcp", "tcp4", "udp6" etc network
// names.
func ipNetOfNetwork(n string) string {
if strings.HasSuffix(n, "4") {
return "ip4"
}
if strings.HasSuffix(n, "6") {
return "ip6"
}
return "ip"
}
// UserDial connects to the provided network address as if a user were initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy)
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.userDialResolve(ctx, network, addr)
if err != nil {
return nil, err
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
// TODO(bradfitz): netns, etc
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
//
// network must a "tcp" type, and addr must be an ip:port. Name resolution
// is not supported.
func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, fmt.Errorf("peerAPI dial requires tcp; %q not supported", network)
}
ipp, err := netaddr.ParseIPPort(addr)
if err != nil {
return nil, fmt.Errorf("peerAPI dial requires ip:port, not name resolution: %w", err)
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
return d.getPeerDialer().DialContext(ctx, network, addr)
}
// getPeerDialer returns the *net.Dialer to use to dial peers to use
// peer API.
//
// This is not used in netstack mode.
//
// The primary function of this is to work on macOS & iOS's in the
// Network/System Extension so it can mark the dialer as staying
// withing the network namespace/sandbox.
func (d *Dialer) getPeerDialer() *net.Dialer {
d.peerDialerOnce.Do(func() {
d.peerDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
Control: d.PeerDialControlFunc(),
}
})
return d.peerDialer
}
// PeerAPIHTTPClient returns an HTTP Client to call peers' peerapi
// endpoints. //
// The returned Client must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPIHTTPClient() *http.Client {
d.peerClientOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Dial = nil
t.DialContext = d.dialPeerAPI
d.peerClient = &http.Client{Transport: t}
})
return d.peerClient
}
// PeerAPITransport returns a Transport to call peers' peerapi
// endpoints.
//
// The returned value must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPITransport() *http.Transport {
return d.PeerAPIHTTPClient().Transport.(*http.Transport)
}

View File

@@ -119,13 +119,13 @@ func ensureStateDirPerms(dirPath string) error {
// We configure the DACL such that any files or directories created within
// dirPath will also inherit this DACL.
explicitAccess := []windows.EXPLICIT_ACCESS{
windows.EXPLICIT_ACCESS{
{
windows.GENERIC_ALL,
windows.SET_ACCESS,
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
userTrustee,
},
windows.EXPLICIT_ACCESS{
{
windows.GENERIC_ALL,
windows.SET_ACCESS,
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,

View File

@@ -15,13 +15,13 @@ func TestParsePort(t *testing.T) {
expect int
}
tests := []InOut{
InOut{"1.2.3.4:5678", 5678},
InOut{"0.0.0.0.999", 999},
InOut{"1.2.3.4:*", 0},
InOut{"5.5.5.5:0", 0},
InOut{"[1::2]:5", 5},
InOut{"[1::2].5", 5},
InOut{"gibberish", -1},
{"1.2.3.4:5678", 5678},
{"0.0.0.0.999", 999},
{"1.2.3.4:*", 0},
{"5.5.5.5:0", 0},
{"[1::2]:5", 5},
{"[1::2].5", 5},
{"gibberish", -1},
}
for _, io := range tests {

View File

@@ -48,7 +48,9 @@ func TestBasics(t *testing.T) {
}()
go func() {
c, err := Connect(sock, port)
s := DefaultConnectionStrategy(sock)
s.UsePort(port)
c, err := Connect(s)
if err != nil {
errs <- err
return

View File

@@ -11,8 +11,8 @@ import (
"syscall"
)
func connect(path string, port uint16) (net.Conn, error) {
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
func connect(s *ConnectionStrategy) (net.Conn, error) {
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
if err != nil {
return nil, err
}

View File

@@ -57,10 +57,65 @@ func tailscaledStillStarting() bool {
return tailscaledProcExists()
}
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
func Connect(path string, port uint16) (net.Conn, error) {
// A ConnectionStrategy is a plan for how to connect to tailscaled or equivalent (e.g. IPNExtension on macOS).
type ConnectionStrategy struct {
// For now, a ConnectionStrategy is just a unix socket path, a TCP port,
// and a flag indicating whether to try fallback connections options.
path string
port uint16
fallback bool
// Longer term, a ConnectionStrategy should be an ordered list of things to attempt,
// with just the information required to connection for each.
//
// We have at least these cases to consider (see issue 3530):
//
// tailscale sandbox | tailscaled sandbox | OS | connection
// ------------------|--------------------|---------|-----------
// no | no | unix | unix socket
// no | no | Windows | TCP/port
// no | no | wasm | memconn
// no | Network Extension | macOS | TCP/port/token, port/token from lsof
// no | System Extension | macOS | TCP/port/token, port/token from lsof
// yes | Network Extension | macOS | TCP/port/token, port/token from readdir
// yes | System Extension | macOS | TCP/port/token, port/token from readdir
//
// Note e.g. that port is only relevant as an input to Connect on Windows,
// that path is not relevant to Windows, and that neither matters to wasm.
}
// DefaultConnectionStrategy returns a default connection strategy.
// The default strategy is to attempt to connect in as many ways as possible.
// It uses path as the unix socket path, when applicable,
// and defaults to WindowsLocalPort for the TCP port when applicable.
// It falls back to auto-discovery across sandbox boundaries on macOS.
// TODO: maybe take no arguments, since path is irrelevant on Windows? Discussion in PR 3499.
func DefaultConnectionStrategy(path string) *ConnectionStrategy {
return &ConnectionStrategy{path: path, port: WindowsLocalPort, fallback: true}
}
// UsePort modifies s to use port for the TCP port when applicable.
// UsePort is only applicable on Windows, and only then
// when not using the default for Windows.
func (s *ConnectionStrategy) UsePort(port uint16) {
s.port = port
}
// UseFallback modifies s to set whether it should fall back
// to connecting to the macOS GUI's tailscaled
// if the Unix socket path wasn't reachable.
func (s *ConnectionStrategy) UseFallback(b bool) {
s.fallback = b
}
// ExactPath returns a connection strategy that only attempts to connect via path.
func ExactPath(path string) *ConnectionStrategy {
return &ConnectionStrategy{path: path, fallback: false}
}
// Connect connects to tailscaled using s
func Connect(s *ConnectionStrategy) (net.Conn, error) {
for {
c, err := connect(path, port)
c, err := connect(s)
if err != nil && tailscaledStillStarting() {
time.Sleep(250 * time.Millisecond)
continue

View File

@@ -17,6 +17,6 @@ func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error)
return ln, 1, err
}
func connect(path string, port uint16) (net.Conn, error) {
func connect(_ *ConnectionStrategy) (net.Conn, error) {
return memconn.Dial("memu", memName)
}

View File

@@ -32,6 +32,5 @@ func init() {
}
}
return false
}
}

View File

@@ -23,19 +23,19 @@ import (
)
// TODO(apenwarr): handle magic cookie auth
func connect(path string, port uint16) (net.Conn, error) {
func connect(s *ConnectionStrategy) (net.Conn, error) {
if runtime.GOOS == "js" {
return nil, errors.New("safesocket.Connect not yet implemented on js/wasm")
}
if runtime.GOOS == "darwin" && path == "" && port == 0 {
if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 {
return connectMacOSAppSandbox()
}
pipe, err := net.Dial("unix", path)
pipe, err := net.Dial("unix", s.path)
if err != nil {
if runtime.GOOS == "darwin" {
if runtime.GOOS == "darwin" && s.fallback {
extConn, extErr := connectMacOSAppSandbox()
if extErr != nil {
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", path, err, extErr)
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr)
}
return extConn, nil
}

View File

@@ -32,7 +32,7 @@ main() {
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
. /etc/os-release
case "$ID" in
ubuntu|pop|neon)
ubuntu|pop|neon|zorin|elementary|linuxmint)
OS="ubuntu"
VERSION="$VERSION_CODENAME"
PACKAGETYPE="apt"
@@ -68,6 +68,23 @@ main() {
APT_KEY_TYPE="keyring"
fi
;;
kali)
OS="debian"
PACKAGETYPE="apt"
YEAR="$(echo "$VERSION_ID" | cut -f1 -d.)"
APT_SYSTEMCTL_START=true
# Third-party keyrings became the preferred method of
# installation in Debian 11 (Bullseye), which Kali switched
# to in roughly 2021.x releases
if [ "$YEAR" -lt 2021 ]; then
# Kali VERSION_ID is "kali-rolling", which isn't distinguishing
VERSION="buster"
APT_KEY_TYPE="legacy"
else
VERSION="bullseye"
APT_KEY_TYPE="keyring"
fi
;;
centos)
OS="$ID"
VERSION="$VERSION_ID"
@@ -94,6 +111,11 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
;;
amzn)
OS="amazon-linux"
VERSION="$VERSION_ID"
@@ -386,6 +408,10 @@ main() {
esac
$SUDO apt-get update
$SUDO apt-get install tailscale
if [ "$APT_SYSTEMCTL_START" = "true" ]; then
$SUDO systemctl enable --now tailscaled
$SUDO systemctl start tailscaled
fi
set +x
;;
yum)
@@ -399,7 +425,7 @@ main() {
dnf)
set -x
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
$SUDO dnf install tailscale
$SUDO dnf install -y tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;

View File

@@ -379,18 +379,49 @@ func (h *Hostinfo) CheckRequestTags() error {
return nil
}
// ServiceProto is a service type. It's usually
// TCP ("tcp") or UDP ("udp"), but it can also have
// meta service values as defined in Service.Proto.
type ServiceProto string
const (
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
PeerAPI4 = ServiceProto("peerapi4")
PeerAPI6 = ServiceProto("peerapi6")
PeerAPIDNS = ServiceProto("peerapi-dns-proxy")
)
// Service represents a service running on a node.
type Service struct {
_ structs.Incomparable
Proto ServiceProto // TCP or UDP
Port uint16 // port number service is listening on
Description string `json:",omitempty"` // text description of service
_ structs.Incomparable
// Proto is the type of service. It's usually the constant TCP
// or UDP ("tcp" or "udp"), but it can also be one of the
// following meta service values:
//
// * "peerapi4": peerapi is available on IPv4; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv4 address.
// * "peerapi6": peerapi is available on IPv6; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv6 address.
// * "peerapi-dns": the local peerapi service supports
// being a DNS proxy (when the node is an exit
// node). For this service, the Port number is really
// the version number of the service.
Proto ServiceProto
// Port is the port number.
//
// For Proto "peerapi-dns", it's the version number of the DNS proxy,
// currently 1.
Port uint16
// Description is the textual description of the service,
// usually the process name that's running.
Description string `json:",omitempty"`
// TODO(apenwarr): allow advertising services on subnet IPs?
// TODO(apenwarr): add "tags" here for each service?
}
@@ -907,6 +938,21 @@ type DNSConfig struct {
// ExtraRecords contains extra DNS records to add to the
// MagicDNS config.
ExtraRecords []DNSRecord `json:",omitempty"`
// ExitNodeFilteredSuffixes are the the DNS suffixes that the
// node, when being an exit node DNS proxy, should not answer.
//
// The entries do not contain trailing periods and are always
// all lowercase.
//
// If an entry starts with a period, it's a suffix match (but
// suffix ".a.b" doesn't match "a.b"; a prefix is required).
//
// If an entry does not start with a period, it's an exact
// match.
//
// Matches are case insensitive.
ExitNodeFilteredSet []string
}
// DNSRecord is an extra DNS record to add to MagicDNS.

View File

@@ -208,20 +208,22 @@ func (src *DNSConfig) Clone() *DNSConfig {
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
dst.CertDomains = append(src.CertDomains[:0:0], src.CertDomains...)
dst.ExtraRecords = append(src.ExtraRecords[:0:0], src.ExtraRecords...)
dst.ExitNodeFilteredSet = append(src.ExitNodeFilteredSet[:0:0], src.ExitNodeFilteredSet...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
Resolvers []dnstype.Resolver
Routes map[string][]dnstype.Resolver
FallbackResolvers []dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netaddr.IP
PerDomain bool
CertDomains []string
ExtraRecords []DNSRecord
Resolvers []dnstype.Resolver
Routes map[string][]dnstype.Resolver
FallbackResolvers []dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netaddr.IP
PerDomain bool
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
}{})
// Clone makes a deep copy of RegisterResponse.

View File

@@ -8,6 +8,7 @@
package tsnet
import (
"context"
"errors"
"fmt"
"log"
@@ -20,12 +21,14 @@ import (
"sync"
"time"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/net/nettest"
"tailscale.com/net/tsdial"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
@@ -114,9 +117,11 @@ func (s *Server) start() error {
return err
}
dialer := new(tsdial.Dialer) // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
ListenPort: 0,
LinkMonitor: linkMon,
Dialer: dialer,
})
if err != nil {
return err
@@ -127,7 +132,7 @@ func (s *Server) start() error {
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn)
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer)
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
@@ -136,6 +141,13 @@ func (s *Server) start() error {
if err := ns.Start(); err != nil {
return fmt.Errorf("failed to start netstack: %w", err)
}
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := eng.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
statePath := filepath.Join(s.dir, "tailscaled.state")
store, err := ipn.NewFileStore(statePath)
@@ -144,7 +156,7 @@ func (s *Server) start() error {
}
logid := "tslib-TODO"
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}

View File

@@ -29,6 +29,7 @@ import (
"testing"
"time"
"github.com/klauspost/compress/zstd"
"go4.org/mem"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -42,38 +43,68 @@ import (
"tailscale.com/version"
)
// Binaries are the paths to a tailscaled and tailscale binary.
// These can be shared by multiple nodes.
type Binaries struct {
Dir string // temp dir for tailscale & tailscaled
Daemon string // tailscaled
CLI string // tailscale
}
// BuildTestBinaries builds tailscale and tailscaled, failing the test
// if they fail to compile.
func BuildTestBinaries(t testing.TB) *Binaries {
td := t.TempDir()
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
return &Binaries{
Dir: td,
Daemon: filepath.Join(td, "tailscaled"+exe()),
CLI: filepath.Join(td, "tailscale"+exe()),
// CleanupBinaries cleans up any resources created by calls to BinaryDir, TailscaleBinary, or TailscaledBinary.
// It should be called from TestMain after all tests have completed.
func CleanupBinaries() {
buildOnce.Do(func() {})
if binDir != "" {
os.RemoveAll(binDir)
}
}
// buildMu limits our use of "go build" to one at a time, so we don't
// fight Go's built-in caching trying to do the same build concurrently.
var buildMu sync.Mutex
// BinaryDir returns a directory containing test tailscale and tailscaled binaries.
// If any test calls BinaryDir, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func BinaryDir(tb testing.TB) string {
buildOnce.Do(func() {
binDir, buildErr = buildTestBinaries()
})
if buildErr != nil {
tb.Fatal(buildErr)
}
return binDir
}
func build(t testing.TB, outDir string, targets ...string) {
buildMu.Lock()
defer buildMu.Unlock()
// TailscaleBinary returns the path to the test tailscale binary.
// If any test calls TailscaleBinary, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func TailscaleBinary(tb testing.TB) string {
return filepath.Join(BinaryDir(tb), "tailscale"+exe())
}
t0 := time.Now()
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
// TailscaledBinary returns the path to the test tailscaled binary.
// If any test calls TailscaleBinary, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func TailscaledBinary(tb testing.TB) string {
return filepath.Join(BinaryDir(tb), "tailscaled"+exe())
}
goBin := findGo(t)
var (
buildOnce sync.Once
buildErr error
binDir string
)
// buildTestBinaries builds tailscale and tailscaled.
// It returns the dir containing the binaries.
func buildTestBinaries() (string, error) {
bindir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
err = build(bindir, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
if err != nil {
os.RemoveAll(bindir)
return "", err
}
return bindir, nil
}
func build(outDir string, targets ...string) error {
goBin, err := findGo()
if err != nil {
return err
}
cmd := exec.Command(goBin, "install")
if version.IsRace() {
cmd.Args = append(cmd.Args, "-race")
@@ -82,7 +113,7 @@ func build(t testing.TB, outDir string, targets ...string) {
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
errOut, err := cmd.CombinedOutput()
if err == nil {
return
return nil
}
if strings.Contains(string(errOut), "when GOBIN is set") {
// Fallback slow path for cross-compiled binaries.
@@ -91,25 +122,25 @@ func build(t testing.TB, outDir string, targets ...string) {
cmd := exec.Command(goBin, "build", "-o", outFile, target)
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
if errOut, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
}
}
return
return nil
}
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
return fmt.Errorf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
}
func findGo(t testing.TB) string {
func findGo() (string, error) {
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
if fi, err := os.Stat(goBin); err != nil {
if os.IsNotExist(err) {
t.Fatalf("failed to find go at %v", goBin)
return "", fmt.Errorf("failed to find go at %v", goBin)
}
t.Fatalf("looking for go binary: %v", err)
return "", fmt.Errorf("looking for go binary: %v", err)
} else if !fi.Mode().IsRegular() {
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
return "", fmt.Errorf("%v is unexpected %v", goBin, fi.Mode())
}
return goBin
return goBin, nil
}
func exe() string {
@@ -239,12 +270,15 @@ func (lc *LogCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body io.Reader = r.Body
if r.Header.Get("Content-Encoding") == "zstd" {
var err error
body, err = smallzstd.NewDecoder(body)
var dec *zstd.Decoder
dec, err = smallzstd.NewDecoder(body)
if err != nil {
log.Printf("bad caught zstd: %v", err)
http.Error(w, err.Error(), 400)
return
}
defer dec.Close()
body = dec
}
bodyBytes, _ := ioutil.ReadAll(body)

View File

@@ -52,6 +52,7 @@ func TestMain(m *testing.M) {
os.Setenv("TS_DISABLE_UPNP", "true")
flag.Parse()
v := m.Run()
CleanupBinaries()
if v != 0 {
os.Exit(v)
}
@@ -62,17 +63,12 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
func TestOneNodeUp_NoAuth(t *testing.T) {
func TestOneNodeUpNoAuth(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
@@ -86,15 +82,10 @@ func TestOneNodeUp_NoAuth(t *testing.T) {
func TestOneNodeExpiredKey(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
n1.AwaitRunning(t)
@@ -127,14 +118,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
func TestCollectPanic(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n := newTestNode(t, env)
cmd := exec.Command(n.env.Binaries.Daemon, "--cleanup")
cmd := exec.Command(env.daemon, "--cleanup")
cmd.Env = append(os.Environ(),
"TS_PLEASE_PANIC=1",
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
@@ -143,7 +130,7 @@ func TestCollectPanic(t *testing.T) {
t.Logf("initial run: %s", got)
// Now we run it again, and on start, it will upload the logs to logcatcher.
cmd = exec.Command(n.env.Binaries.Daemon, "--cleanup")
cmd = exec.Command(env.daemon, "--cleanup")
cmd.Env = append(os.Environ(), "TS_LOG_TARGET="+n.env.LogCatcherServer.URL)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("cleanup failed: %v: %q", err, out)
@@ -162,15 +149,10 @@ func TestCollectPanic(t *testing.T) {
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
func TestStateSavedOnStart(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
@@ -201,18 +183,14 @@ func TestStateSavedOnStart(t *testing.T) {
d1.MustCleanShutdown(t)
}
func TestOneNodeUp_Auth(t *testing.T) {
func TestOneNodeUpAuth(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins, configureControl(func(control *testcontrol.Server) {
env := newTestEnv(t, configureControl(func(control *testcontrol.Server) {
control.RequireAuth = true
}))
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitListening(t)
@@ -250,21 +228,16 @@ func TestOneNodeUp_Auth(t *testing.T) {
func TestTwoNodes(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
// Create two nodes:
n1 := newTestNode(t, env)
n1SocksAddrCh := n1.socks5AddrChan()
d1 := n1.StartDaemon(t)
defer d1.Kill()
n2 := newTestNode(t, env)
n2SocksAddrCh := n2.socks5AddrChan()
d2 := n2.StartDaemon(t)
defer d2.Kill()
n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh)
@@ -301,14 +274,9 @@ func TestTwoNodes(t *testing.T) {
func TestNodeAddressIPFields(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitListening(t)
n1.MustUp()
@@ -332,14 +300,9 @@ func TestNodeAddressIPFields(t *testing.T) {
func TestAddPingRequest(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.StartDaemon(t)
n1.AwaitListening(t)
n1.MustUp()
@@ -391,15 +354,10 @@ func TestAddPingRequest(t *testing.T) {
// be connected to control.
func TestNoControlConnWhenDown(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
// Come up the first time.
@@ -413,7 +371,6 @@ func TestNoControlConnWhenDown(t *testing.T) {
env.LogCatcher.Reset()
d2 := n1.StartDaemon(t)
defer d2.Kill()
n1.AwaitResponding(t)
st := n1.MustStatus(t)
@@ -438,16 +395,11 @@ func TestNoControlConnWhenDown(t *testing.T) {
// without the GUI to kick off a Start.
func TestOneNodeUpWindowsStyle(t *testing.T) {
t.Parallel()
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp("--unattended")
@@ -460,8 +412,9 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
// testEnv contains the test environment (set of servers) used by one
// or more nodes.
type testEnv struct {
t testing.TB
Binaries *Binaries
t testing.TB
cli string
daemon string
LogCatcher *LogCatcher
LogCatcherServer *httptest.Server
@@ -483,11 +436,9 @@ func (f configureControl) modifyTestEnv(te *testEnv) {
f(te.Control)
}
// newTestEnv starts a bunch of services and returns a new test
// environment.
//
// Call Close to shut everything down.
func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
// newTestEnv starts a bunch of services and returns a new test environment.
// newTestEnv arranges for the environment's resources to be cleaned up on exit.
func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
if runtime.GOOS == "windows" {
t.Skip("not tested/working on Windows yet")
}
@@ -500,7 +451,8 @@ func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
trafficTrap := new(trafficTrap)
e := &testEnv{
t: t,
Binaries: bins,
cli: TailscaleBinary(t),
daemon: TailscaledBinary(t),
LogCatcher: logc,
LogCatcherServer: httptest.NewServer(logc),
Control: control,
@@ -512,21 +464,19 @@ func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
o.modifyTestEnv(e)
}
control.HTTPTestServer.Start()
t.Cleanup(func() {
// Shut down e.
if err := e.TrafficTrap.Err(); err != nil {
e.t.Errorf("traffic trap: %v", err)
e.t.Logf("logs: %s", e.LogCatcher.logsString())
}
e.LogCatcherServer.Close()
e.TrafficTrapServer.Close()
e.ControlServer.Close()
})
return e
}
func (e *testEnv) Close() error {
if err := e.TrafficTrap.Err(); err != nil {
e.t.Errorf("traffic trap: %v", err)
e.t.Logf("logs: %s", e.LogCatcher.logsString())
}
e.LogCatcherServer.Close()
e.TrafficTrapServer.Close()
e.ControlServer.Close()
return nil
}
// testNode is a machine with a tailscale & tailscaled.
// Currently, the test is simplistic and user==node==machine.
// That may grow complexity later to test more.
@@ -681,10 +631,6 @@ type Daemon struct {
Process *os.Process
}
func (d *Daemon) Kill() {
d.Process.Kill()
}
func (d *Daemon) MustCleanShutdown(t testing.TB) {
d.Process.Signal(os.Interrupt)
ps, err := d.Process.Wait()
@@ -696,14 +642,14 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
}
}
// StartDaemon starts the node's tailscaled, failing if it fails to
// start.
// StartDaemon starts the node's tailscaled, failing if it fails to start.
// StartDaemon ensures that the process will exit when the test completes.
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
return n.StartDaemonAsIPNGOOS(t, runtime.GOOS)
}
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
cmd := exec.Command(n.env.Binaries.Daemon,
cmd := exec.Command(n.env.daemon,
"--tun=userspace-networking",
"--state="+n.stateFile,
"--socket="+n.sockFile,
@@ -724,6 +670,7 @@ func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
if err := cmd.Start(); err != nil {
t.Fatalf("starting tailscaled: %v", err)
}
t.Cleanup(func() { cmd.Process.Kill() })
return &Daemon{
Process: cmd.Process,
}
@@ -736,8 +683,11 @@ func (n *testNode) MustUp(extraArgs ...string) {
"--login-server=" + n.env.ControlServer.URL,
}
args = append(args, extraArgs...)
t.Logf("Running %v ...", args)
if b, err := n.Tailscale(args...).CombinedOutput(); err != nil {
cmd := n.Tailscale(args...)
t.Logf("Running %v ...", cmd)
cmd.Stdout = nil // in case --verbose-tailscale was set
cmd.Stderr = nil // in case --verbose-tailscale was set
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("up: %v, %v", string(b), err)
}
}
@@ -753,8 +703,10 @@ func (n *testNode) MustDown() {
// AwaitListening waits for the tailscaled to be serving local clients
// over its localhost IPC mechanism. (Unix socket, etc)
func (n *testNode) AwaitListening(t testing.TB) {
s := safesocket.DefaultConnectionStrategy(n.sockFile)
s.UseFallback(false) // connect only to the tailscaled that we started
if err := tstest.WaitFor(20*time.Second, func() (err error) {
c, err := safesocket.Connect(n.sockFile, safesocket.WindowsLocalPort)
c, err := safesocket.Connect(s)
if err != nil {
return err
}
@@ -841,7 +793,7 @@ func (n *testNode) AwaitNeedsLogin(t testing.TB) {
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
// It does not start the process.
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
cmd := exec.Command(n.env.Binaries.CLI, "--socket="+n.sockFile)
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
cmd.Args = append(cmd.Args, arg...)
cmd.Dir = n.dir
cmd.Env = append(os.Environ(),

View File

@@ -22,7 +22,9 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,7 +22,9 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,7 +22,9 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,7 +22,9 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -26,7 +26,9 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -35,7 +35,9 @@ import (
type Harness struct {
testerDialer proxy.Dialer
testerDir string
bins *integration.Binaries
binaryDir string
cli string
daemon string
pubKey string
signer ssh.Signer
cs *testcontrol.Server
@@ -134,11 +136,11 @@ func newHarness(t *testing.T) *Harness {
loginServer := fmt.Sprintf("http://%s", ln.Addr())
t.Logf("loginServer: %s", loginServer)
bins := integration.BuildTestBinaries(t)
h := &Harness{
pubKey: string(pubkey),
bins: bins,
binaryDir: integration.BinaryDir(t),
cli: integration.TailscaleBinary(t),
daemon: integration.TailscaledBinary(t),
signer: signer,
loginServerURL: loginServer,
cs: cs,
@@ -146,7 +148,7 @@ func newHarness(t *testing.T) *Harness {
ipMap: ipMap,
}
h.makeTestNode(t, bins, loginServer)
h.makeTestNode(t, loginServer)
return h
}
@@ -156,7 +158,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
args = append([]string{"--socket=" + filepath.Join(h.testerDir, "sock")}, args...)
cmd := exec.Command(h.bins.CLI, args...)
cmd := exec.Command(h.cli, args...)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err)
@@ -169,7 +171,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
// enables us to make connections to and from the tailscale network being
// tested. This mutates the Harness to allow tests to dial into the tailscale
// network as well as control the tester's tailscaled.
func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, controlURL string) {
func (h *Harness) makeTestNode(t *testing.T, controlURL string) {
dir := t.TempDir()
h.testerDir = dir
@@ -179,7 +181,7 @@ func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, control
}
cmd := exec.Command(
bins.Daemon,
h.daemon,
"--tun=userspace-networking",
"--state="+filepath.Join(dir, "state.json"),
"--socket="+filepath.Join(dir, "sock"),
@@ -222,7 +224,7 @@ outer:
}
}
run(t, dir, bins.CLI,
run(t, dir, h.cli,
"--socket="+filepath.Join(dir, "sock"),
"up",
"--login-server="+controlURL,

View File

@@ -17,7 +17,6 @@ import (
"testing"
"text/template"
"tailscale.com/tstest/integration"
"tailscale.com/types/logger"
)
@@ -153,15 +152,15 @@ in {
systemd.services.tailscaled.environment."TS_LOG_TARGET" = "{{.LogTarget}}";
}`
func copyUnit(t *testing.T, bins *integration.Binaries) {
func (h *Harness) copyUnit(t *testing.T) {
t.Helper()
data, err := os.ReadFile("../../../cmd/tailscaled/tailscaled.service")
if err != nil {
t.Fatal(err)
}
os.MkdirAll(filepath.Join(bins.Dir, "systemd"), 0755)
err = os.WriteFile(filepath.Join(bins.Dir, "systemd", "tailscaled.service"), data, 0666)
os.MkdirAll(filepath.Join(h.binaryDir, "systemd"), 0755)
err = os.WriteFile(filepath.Join(h.binaryDir, "systemd", "tailscaled.service"), data, 0666)
if err != nil {
t.Fatal(err)
}
@@ -172,7 +171,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
t.Skip("https://github.com/NixOS/nixpkgs/issues/131098")
}
copyUnit(t, h.bins)
h.copyUnit(t)
dir := t.TempDir()
fname := filepath.Join(dir, d.Name+".nix")
fout, err := os.Create(fname)
@@ -185,7 +184,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
BinPath string
LogTarget string
}{
BinPath: h.bins.Dir,
BinPath: h.binaryDir,
LogTarget: h.loginServerURL,
})
if err != nil {

View File

@@ -9,6 +9,7 @@ package vms
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
@@ -23,10 +24,10 @@ import (
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"tailscale.com/types/logger"
@@ -171,21 +172,19 @@ func fetchFromS3(t *testing.T, fout *os.File, d Distro) bool {
return false
}
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
})
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1"))
if err != nil {
t.Logf("can't make AWS session: %v", err)
t.Logf("can't load AWS credentials: %v", err)
return false
}
dler := s3manager.NewDownloader(sess, func(d *s3manager.Downloader) {
dler := manager.NewDownloader(s3.NewFromConfig(cfg), func(d *manager.Downloader) {
d.PartSize = 64 * 1024 * 1024 // 64MB per part
})
t.Logf("fetching s3://%s/%s", bucketName, d.SHA256Sum)
_, err = dler.Download(fout, &s3.GetObjectInput{
_, err = dler.Download(context.TODO(), fout, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(d.SHA256Sum),
})
@@ -291,7 +290,6 @@ func checkCachedImageHash(t *testing.T, d Distro, cacheDir string) string {
}
func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
bins := h.bins
if strings.HasPrefix(d.Name, "nixos") {
return
}
@@ -306,8 +304,8 @@ func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
mkdir(t, cli, "/etc/default")
mkdir(t, cli, "/var/lib/tailscale")
copyFile(t, cli, bins.Daemon, "/usr/sbin/tailscaled")
copyFile(t, cli, bins.CLI, "/usr/bin/tailscale")
copyFile(t, cli, h.daemon, "/usr/sbin/tailscaled")
copyFile(t, cli, h.cli, "/usr/bin/tailscale")
// TODO(Xe): revisit this assumption before it breaks the test.
copyFile(t, cli, "../../../cmd/tailscaled/tailscaled.defaults", "/etc/default/tailscaled")

View File

@@ -30,6 +30,7 @@ import (
"golang.org/x/sync/semaphore"
"inet.af/netaddr"
"tailscale.com/tstest"
"tailscale.com/tstest/integration"
"tailscale.com/types/logger"
)
@@ -52,6 +53,13 @@ var (
}()
)
func TestMain(m *testing.M) {
flag.Parse()
v := m.Run()
integration.CleanupBinaries()
os.Exit(v)
}
func TestDownloadImages(t *testing.T) {
if !*runVMTests {
t.Skip("not running integration tests (need --run-vm-tests)")

View File

@@ -5,6 +5,7 @@
// This file exists just so go mod tidy won't remove
// staticcheck's module from our go.mod.
//go:build tools
// +build tools
package tstest

View File

@@ -18,6 +18,8 @@ import (
"strings"
"sync"
"time"
"context"
)
// Logf is the basic Tailscale logger type: a printf-like func.
@@ -25,6 +27,27 @@ import (
// Logf functions must be safe for concurrent use.
type Logf func(format string, args ...interface{})
// A Context is a context.Context that should contain a custom log function, obtainable from FromContext.
// If no log function is present, FromContext will return log.Printf.
// To construct a Context, use Add
type Context context.Context
type logfKey struct{}
// FromContext extracts a log function from ctx.
func FromContext(ctx Context) Logf {
v := ctx.Value(logfKey{})
if v == nil {
return log.Printf
}
return v.(Logf)
}
// Ctx constructs a Context from ctx with fn as its custom log function.
func Ctx(ctx context.Context, fn Logf) Context {
return context.WithValue(ctx, logfKey{}, fn)
}
// WithPrefix wraps f, prefixing each format with the provided prefix.
func WithPrefix(f Logf, prefix string) Logf {
return func(format string, args ...interface{}) {

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