Compare commits

...

395 Commits

Author SHA1 Message Date
Shayne Sweeney
1dc9edde90 cmd/tailscale/cli: [web] update JS in web.html for Unraid support
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-05-16 20:34:30 -04:00
Derek Kaser
b15d8525d0 cmd/tailscale: allow Tailscale to work with Unraid web interface
Updates tailscale/tailscale#8026

Signed-off-by: Derek Kaser <derek.kaser@gmail.com>
2023-05-06 22:35:02 -04:00
Derek Kaser
0d7303b798 various: add detection and Taildrop for Unraid
Updates tailscale/tailscale#8025

Signed-off-by: Derek Kaser <derek.kaser@gmail.com>
2023-05-04 13:40:13 -07:00
Flakes Updater
d1ce7a9b5e go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-05-04 12:55:01 -07:00
James Tucker
5def4f4a1c go.mod: bump goreleaser deps
Periodic update for start of cycle. goreleaser is not updated to v2 yet,
but indirects updated.

Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2023-05-04 12:32:24 -07:00
Flakes Updater
1c6ff310ae go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-05-04 12:31:31 -07:00
James Tucker
48605226dd go.mod: bump gvisor
Periodic update for start of cycle.

Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2023-05-04 12:30:27 -07:00
Maisem Ali
f46c1aede0 go.mod: bump k8s libs
The key is to update sigs.k8s.io/controller-runtime and let it update others.

Updates #8043

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-05-04 12:00:03 -07:00
Brad Fitzpatrick
73d128238e envknob: support tailscaled-env.txt on macOS too
Updates #3707

Co-authored-by: Marwan Sulaiman <marwan@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-04 10:27:59 -07:00
Anton Tolchanov
787fc41fa4 scripts/installer.sh: check connectivity with pkgs.tailscale.com
Installer script relies on pkgs.tailscale.com being reachable, both for
checking what Linux distros are supported, but also for actually
downloading repo configuration files, gpg keys and packages themselves.

This change adds a simple reachability check which will print an error
message when pkgs.tailscale.com is not reachable.

Fixes https://github.com/tailscale/corp/issues/8952

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-05-04 16:49:56 +02:00
Flakes Updater
5783adcc6f go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-05-03 19:56:23 -07:00
License Updater
503b6dd8be licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-05-03 19:56:00 -07:00
Brad Fitzpatrick
9e9ea6e974 go.mod: bump all deps possible that don't break the build
This holds back gvisor, kubernetes, goreleaser, and esbuild, which all
had breaking API changes.

Updates #8043
Updates #7381
Updates #8042 (updates u-root which adds deps)

Change-Id: I889759bea057cd3963037d41f608c99eb7466a5b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-03 19:52:54 -07:00
M. J. Fromberger
459744c9ea .github: mark bots for exemption by issuebot (#8041)
#cleanup

Change-Id: I13757ee20240166af72606d166e840ae3ee797d7
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2023-05-03 18:36:19 -07:00
License Updater
7675d323fa licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-05-03 15:48:09 -07:00
dependabot[bot]
270942094f build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.16+incompatible to 20.10.24+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.16...v20.10.24)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 15:40:46 -07:00
Maisem Ali
be190e990f ssh/tailssh: restore support for recording locally
We removed it earlier in 916aa782af, but we still want to support it for some time longer.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-05-03 15:00:01 -07:00
Brad Fitzpatrick
4d7927047c wgengine/magicsock: annotate, skip flaky TestIsWireGuardOnlyPickEndpointByPing
Updates #8037
Updates #7826

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-03 14:58:28 -07:00
Charlotte Brandhorst-Satzkorn
ddb4040aa0 wgengine/magicsock: add address selection for wireguard only endpoints (#7979)
This change introduces address selection for wireguard only endpoints.
If a endpoint has not been used before, an address is randomly selected
to be used based on information we know about, such as if they are able
to use IPv4 or IPv6. When an address is initially selected, we also
initiate a new ICMP ping to the endpoints addresses to determine which
endpoint offers the best latency. This information is then used to
update which endpoint we should be using based on the best possible
route. If the latency is the same for a IPv4 and an IPv6 address, IPv6
will be used.

Updates #7826

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-02 17:49:56 -07:00
Brad Fitzpatrick
c1e6888fc7 derp: add a README.md with some docs
Updates #docs

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-02 13:42:25 -07:00
Maisem Ali
3ae7140690 net/tstun: handle exit nodes in NAT configs
In the case where the exit node requires SNAT, we would SNAT all traffic not just the
traffic meant to go through the exit node. This was a result of the default route being
added to the routing table which would match basically everything.

In this case, we need to account for all peers in the routing table not just the ones
that require NAT.

Fix and add a test.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-05-02 13:03:30 -07:00
Andrew Dunham
bcf7b63d7e wgengine/magicsock: add hysteresis to endpoint selection
Avoid selecting an endpoint as "better" than the current endpoint if the
total latency improvement is less than 1%. This adds some hysteresis to
avoid flapping between endpoints for a minimal improvement in latency.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If8312e1768ea65c4b4d4e13d8de284b3825d7a73
2023-05-02 08:56:16 -07:00
Tom DNetto
c5bf868940 ssh/tailssh: improve debug logging around revoked sessions
Updates https://github.com/tailscale/corp/issues/10943
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-05-01 14:10:16 -07:00
Andrew Dunham
42fd964090 control/controlclient: use dnscache.Resolver for Noise client
This passes the *dnscache.Resolver down from the Direct client into the
Noise client and from there into the controlhttp client. This retains
the Resolver so that it can share state across calls instead of creating
a new resolver.

Updates #4845
Updates #6110

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ia5d6af1870f3b5b5d7dd5685d775dcf300aec7af
2023-05-01 13:22:10 -07:00
License Updater
979d29b5f5 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-05-01 09:13:43 -07:00
Anton Tolchanov
1f4a34588b .github: test installer script in CI in docker
Every time we change `installer.sh`, run it in a few docker
containers based on different Linux distros, just as a simple test.

Also includes a few changes to the installer script itself to make
installation work in docker:
- install dnf config-manager command before running it
- run zypper in non-interactive mode
- update pacman indexes before installing packages

Updates https://github.com/tailscale/corp/issues/8952

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-04-29 08:48:19 +01:00
Denton Gentry
a82f275619 cmd/sniproxy: Set App name in tsnet hostinfo
Updates #1748
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-04-28 21:50:30 -07:00
James Tucker
b3c3a9f174 syncs: add Map.Len to get the length of the Map
I need this for a corp change where I have a set as a queue, and make a
different decisison if the set is empty.

Updates tailscale/corp#10344

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-27 19:10:28 -07:00
ayanamist
042f82ea32 build_dist.sh: make cross-compilation friendly for env CC specified
Signed-off-by: ayanamist <ayanamist@gmail.com>
2023-04-27 18:51:59 -07:00
dependabot[bot]
633d08bd7b .github: Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-27 17:21:59 -07:00
James Tucker
d35ce1add9 syncs: add documentation to Map.Range
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-27 17:07:07 -07:00
Charlotte Brandhorst-Satzkorn
c3ab36cb9d words: charlotte 1 spell-checking-linter 0 (#7993)
Can't have a dupe when the dupe is wrong. Clearly we need to up
our spell checking game. Did anyone say AI?

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-04-27 08:58:52 -07:00
James Tucker
8032b966a1 .github/workflows: add recency bias to action cache keys
The action cache restore process either matches the restore key pattern
exactly, or uses a matching prefix with the most recent date.

If the restore key is an exact match, then no updates are uploaded, but
if we've just computed tests executions for more recent code then we
will likely want to use those results in future runs.

Appending run_id to the cache key will give us an always new key, and
then we will be restore a recently uploaded cache that is more likely
has a higher overlap with the code being tested.

Updates #7975

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-26 21:36:53 -07:00
Kyle Carberry
d78b334964 cmd/derper: disable http2
DERP doesn't support HTTP/2. If an HTTP/2 proxy was placed in front of
a DERP server requests would fail because the connection would
be initialized with HTTP/2, which the DERP client doesn't support.

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2023-04-26 20:45:32 -07:00
Charlotte Brandhorst-Satzkorn
161d1d281a net/ping,netcheck: add v6 pinging capabilities to pinger (#7971)
This change adds a v6conn to the pinger to enable sending pings to v6
addrs.

Updates #7826

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-04-26 15:59:37 -07:00
Denton Gentry
1145b9751d VERSION.txt: this is v1.41.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-04-26 15:58:30 -07:00
Brad Fitzpatrick
1e876a3c1d ipn/ipnlocal: fix fmt format arg type mismatch in log line
It was printing like "v0xxxxxxxx" after version.Long became a func
in 8b2ae47c31.

Fixes #7976

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 14:28:09 -07:00
Maisem Ali
a8f10c23b2 cmd/tailscale/cli: [up] reuse --advertise-tags for OAuth key generation
We need to always specify tags when creating an AuthKey from an OAuth key.

Check for that, and reuse the `--advertise-tags` param.

Updates #7982

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-26 14:17:29 -07:00
Brad Fitzpatrick
b2b5379348 cmd/tailscale/cli: [up] change oauth authkey format
Updates #7982

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 13:36:17 -07:00
Brad Fitzpatrick
13de36303d cmd/tailscale/cli: [up] add experimental oauth2 authkey support
Updates #7982

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 12:25:42 -07:00
James Tucker
095d3edd33 ipn/ipnlocal: reenable profile tests on Windows
This fix does not seem ideal, but the test infrastructure using a local
goos doesn't seem to avoid all of the associated challenges, but is
somewhat deeply tied to the setup.

The core issue this addresses for now is that when run on Windows there
can be no code paths that attempt to use an invalid UID string, which on
Windows is described in [1].

For the goos="linux" tests, we now explicitly skip the affected
migration code if runtime.GOOS=="windows", and for the Windows test we
explicitly use the running users uid, rather than just the string
"user1". We also now make the case where a profile exists and has
already been migrated a non-error condition toward the outer API.

Updates #7876

[1] https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-26 11:33:38 -07:00
James Tucker
43819309e1 .github/workflows: split tests and benchmarks for caching
Benchmark flags prevent test caching, so benchmarks are now executed
independently of tests.

Fixes #7975

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-26 10:49:38 -07:00
Maisem Ali
1b8a0dfe5e ssh/tailssh: also handle recording upload failure during writes
Previously we would error out when the recording server disappeared after the in memory
buffer filled up for the io.Copy. This makes it so that we handle failing open correctly
in that path.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-25 19:40:46 -07:00
shayne
018a382729 cmd/tailscale/cli: [serve] fix MinGW path conversion (#7964)
Fixes #7963

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-04-25 13:07:17 -04:00
License Updater
2e07245384 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-25 09:10:26 -07:00
License Updater
aa87e999dc licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-04-25 09:10:05 -07:00
Maisem Ali
f58751eb2b net/packet: add NAT support for DCCP and GRE
Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-24 15:30:50 -07:00
Marwan Sulaiman
ce11c82d51 ipn/store/awsstore: persist state with intelligent tiering
Fixes #6784

This PR makes it so that we can persist the tailscaled state with
intelligent tiering which increases the capacity from 4kb to 8kb

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-04-24 14:35:13 -04:00
Brad Fitzpatrick
90ba26cea1 net/netcheck: fix crash when IPv6 kinda but not really works
Looks like on some systems there's an IPv6 address, but then opening
a IPv6 UDP socket fails later. Probably some firewall. Tolerate it
better and don't crash.

To repro: check the "udp6" to something like "udp7" (something that'll
fail) and run "go run ./cmd/tailscale netcheck" on a machine with
active IPv6. It used to crash and now it doesn't.

Fixes #7949

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-22 17:53:39 -07:00
Maisem Ali
7778d708a6 ssh/tailssh: handle dialing multiple recorders and failing open
This adds support to try dialing out to multiple recorders each
with a 5s timeout and an overall 30s timeout. It also starts respecting
the actions `OnRecordingFailure` field if set, if it is not set
it fails open.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-22 10:23:13 -07:00
Maisem Ali
f66ddb544c tailcfg: add SSHRecorderFailureAction and SSHRecordingFailureNotifyRequest
This allows control to specify how to handle situations where the recorder
isn't available.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-22 10:23:13 -07:00
Flakes Updater
e3b2250e26 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-04-21 21:36:21 -07:00
Andrew Dunham
6f521c138d tailcfg: add CanPort80 field to DERPNode
A follow-up PR will start using this field after we set it in our
production DERPMap.

Updates #7925

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Idb41b79e6055dddb8944f79d91ad4a186ace98c7
2023-04-21 14:15:26 -04:00
Andrew Dunham
04a3118d45 net/tstun: add tests for captureHook
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I630f852d9f16c951c721b34f2bc4128e68fe9475
2023-04-21 14:05:20 -04:00
Denton Gentry
c791e64881 scripts/installer: add Deepin, RisiOS.
Fixes https://github.com/tailscale/tailscale/issues/7862
Fixes https://github.com/tailscale/tailscale/issues/7899

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-04-20 18:51:50 -07:00
Mihai Parparita
7330aa593e all: avoid repeated default interface lookups
On some platforms (notably macOS and iOS) we look up the default
interface to bind outgoing connections to. This is both duplicated
work and results in logspam when the default interface is not available
(i.e. when a phone has no connectivity, we log an error and thus cause
more things that we will try to upload and fail).

Fixed by passing around a netmon.Monitor to more places, so that we can
use its cached interface state.

Fixes #7850
Updates #7621

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-20 15:46:01 -07:00
Will Norris
7f17e04a5a log/sockstatlog: bump logInterval to 10 seconds
We are seeing indications that some devices are still getting into an
upload loop.  Bump logInterval in case these devices are on slow
connections that are taking more than 3 seconds to uploads sockstats.

Updates #7719

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-20 11:37:06 -07:00
Mihai Parparita
4722f7e322 all: move network monitoring from wgengine/monitor to net/netmon
We're using it in more and more places, and it's not really specific to
our use of Wireguard (and does more just link/interface monitoring).

Also removes the separate interface we had for it in sockstats -- it's
a small enough package (we already pull in all of its dependencies
via other paths) that it's not worth the extra complexity.

Updates #7621
Updates #7850

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-20 10:15:59 -07:00
Andrew Dunham
3ede3aafe4 ipn/localapi: also verify STUN queries work in 'debug derp'
Updates #6526

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I274b7ed53ee0be3fb94fdb00cafe06a1d676e1cf
2023-04-20 10:14:59 -04:00
James Tucker
f844791e15 safesocket: enable test to run on Windows unpriviliged
I manually tested that the code path that relaxes pipe permissions is
not executed when run with elevated priviliges, and the test also passes
in that case.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-19 19:06:18 -07:00
James Tucker
cd35a79136 syncs: relax TestWatchMultipleValues timing on Windows
The test is re-enabled for Windows with a relaxed time assertion.

On Windows the runtime poller currently does not have sufficient
resolution to meet the normal requirements for this test.

See https://github.com/golang/go/issues/44343 for background.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-19 19:00:34 -07:00
Andrew Dunham
f85dc6f97c ci: add more lints (#7909)
This is a follow-up to #7905 that adds two more linters and fixes the corresponding findings. As per the previous PR, this only flags things that are "obviously" wrong, and fixes the issues found.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8739bdb7bc4f75666a7385a7a26d56ec13741b7c
2023-04-19 21:54:19 -04:00
dependabot[bot]
5acc7c4b1e .github: Bump ruby/action-slack from 3.0.0 to 3.2.1
Bumps [ruby/action-slack](https://github.com/ruby/action-slack) from 3.0.0 to 3.2.1.
- [Release notes](https://github.com/ruby/action-slack/releases)
- [Commits](https://github.com/ruby/action-slack/compare/v3.0.0...v3.2.1)

---
updated-dependencies:
- dependency-name: ruby/action-slack
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-19 18:41:00 -07:00
dependabot[bot]
c328770184 .github: Bump peter-evans/create-pull-request from 4.2.4 to 5.0.0
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.2.4 to 5.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](38e0b6e68b...5b4a9f6a9e)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-19 18:38:14 -07:00
License Updater
588a234fdc licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-04-19 17:06:12 -07:00
Maisem Ali
c3ef6fb4ee ipn/ipnlocal: handle masquerade addresses in PeerAPI
Without this, the peer fails to do anything over the PeerAPI if it
has a masquerade address.

```
Apr 19 13:58:15 hydrogen tailscaled[6696]: peerapi: invalid request from <ip>:58334: 100.64.0.1/32 not found in self addresses
```

Updates #8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-19 15:51:44 -07:00
Maisem Ali
85de580455 net/tsdial: do not use proxies when dialing out to PeerAPI
Found this when adding a test that does a ping over PeerAPI.

Our integration tests set up a trafficTrap to ensure that tailscaled
does not call out to the internet, and it does so via a HTTP_PROXY.

When adding a test for pings over PeerAPI, it triggered the trap and investigation
lead to the realization that we were not removing the Proxy when trying to
dial out to the PeerAPI.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-19 15:51:44 -07:00
Mihai Parparita
d0906cda97 net/sockstats: expose debug info
Exposes some internal state of the sockstats package via the C2N and
PeerAPI endpoints, so that it can be used for debugging. For now this
includes the estimated radio on percentage and a second-by-second view
of the times the radio was active.

Also fixes another off-by-one error in the radio on percentage that
was leading to >100% values (if n seconds have passed since we started
to monitor, there may be n + 1 possible seconds where the radio could
have been on).

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-19 14:33:12 -07:00
Will Norris
7c386ca6d2 net/sockstats: fix calculation of radio power usage
When splitting the radio monitor usage array, we were splitting at now %
3600 to get values into chronological order.  This caused the value for
the final second to be included at the beginning of the ordered slice
rather than the end.  If there was activity during that final second, an
extra five seconds of high power usage would get recorded in some cases.
This could result in a final calculation of greater than 100% usage.

This corrects that by splitting values at (now+1 % 3600).

This also simplifies the percentage calculation by always rounding
values down, which is sufficient for our usage.

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-19 13:18:02 -07:00
License Updater
7f057d7489 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-19 10:22:36 -07:00
Mihai Parparita
c7cea825ae net/netns: don't log errors when we can't get the default route on Darwin
It's somewhat common (e.g. when a phone has no reception), and leads to
lots of logspam.

Updates #7850

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-18 09:57:15 -07:00
Andrew Dunham
280255acae various: add golangci-lint, fix issues (#7905)
This adds an initial and intentionally minimal configuration for
golang-ci, fixes the issues reported, and adds a GitHub Action to check
new pull requests against this linter configuration.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8f38fbc315836a19a094d0d3e986758b9313f163
2023-04-17 18:38:24 -04:00
Joe Tsai
ff1b35ec6c net/connstats: exclude traffic with internal Tailscale service (#7904)
Exclude traffic with 100.100.100.100 (for IPv4) and
with fd7a:115c:a1e0::53 (for IPv6) since this traffic with the
Tailscale service running locally on the node.
This traffic never left the node.

It also happens to be a high volume amount of traffic since
DNS requests occur over UDP with each request coming from a
unique port, thus resulting in many discrete traffic flows.

Fixes tailscale/corp#10554

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-04-17 14:24:29 -07:00
Mihai Parparita
9a655a1d58 net/dnsfallback: more explicitly pass through logf function
Redoes the approach from #5550 and #7539 to explicitly pass in the logf
function, instead of having global state that can be overridden.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-17 12:06:23 -07:00
dependabot[bot]
28cb1221ba .github: Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-17 11:51:30 -07:00
Brad Fitzpatrick
d5a870b4dc wgengine/monitor: add --monitor-duration flag to opt-in TestMonitorMode
TestMonitorMode skips by default, without the --monitor flag, and then
it previously ran forever. This adds an option --monitor-duration flag
that defaults to zero (run forever) but if non-zero bounds how long
the tests runs. This means you can then also use e.g. `go test
--cpuprofile` and capture a CPU/mem profile for a minute or two.

Updates #7621

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-17 11:14:40 -07:00
Brad Fitzpatrick
162488a775 net/interfaces: cache "home" router lookup on big Linux routers
This is a continuation of the earlier 2a67beaacf but more aggressive;
this now remembers that we failed to find the "home" router IP so we
don't try again later on the next call.

Updates #7621

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-17 10:51:21 -07:00
James Tucker
c5150eae67 net/netcheck: reenable TestNodeAddrResolve on Windows
Updates #7876

Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: Idb2e6cc2edf6ca123b751d6c8f8729b0cba86023
2023-04-17 12:41:56 -04:00
Andrew Dunham
80b138f0df wgengine/magicsock: keep advertising endpoints after we stop discovering them
Previously, when updating endpoints we would immediately stop
advertising any endpoint that wasn't discovered during
determineEndpoints. This could result in, for example, a case where we
performed an incremental netcheck, didn't get any of our three STUN
packets back, and then dropped our STUN endpoint from the set of
advertised endpoints... which would result in clients falling back to a
DERP connection until the next call to determineEndpoints.

Instead, let's cache endpoints that we've discovered and continue
reporting them to clients until a timeout expires. In the above case
where we temporarily don't have a discovered STUN endpoint, we would
continue reporting the old value, then re-discover the STUN endpoint
again and continue reporting it as normal, so clients never see a
withdrawal.

Updates tailscale/coral#108

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I42de72e7418ab328a6c732bdefc74549708cf8b9
2023-04-17 11:26:02 -04:00
Brad Fitzpatrick
4b49ca4a12 wgengine/magicsock: update comments on what implements conn.Bind
The comment still said *magicsock.Conn implemented wireguard-go conn.Bind.
That wasn't accurate anymore.

A doc #cleanup.

Change-Id: I7fd003b939497889cc81147bfb937b93e4f6865c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-16 09:07:13 -07:00
Brad Fitzpatrick
10f1c90f4d wgengine/magicsock, types/nettype, etc: finish ReadFromUDPAddrPort netip migration
So we're staying within the netip.Addr/AddrPort consistently and
avoiding allocs/conversions to the legacy net addr types.

Updates #5162

Change-Id: I59feba60d3de39f773e68292d759766bac98c917
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-15 13:40:15 -07:00
Brad Fitzpatrick
29f7df9d8f wgengine/magicsock, etc: remove mostly unused WriteTo methods
Updates #2331
Updates #5162

Change-Id: I8291884425481eeaedde38a54adfd8ed7292a497
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-15 08:32:11 -07:00
Brad Fitzpatrick
83c41f3697 net/netcheck: remove unused method from interface
Updates #2331
Updates #5162

Change-Id: I77ff956c2d59bde611d47523659a07afb4a6da2d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-15 07:55:43 -07:00
James Tucker
20f17d6e7b wgengine/magicsock: reenable magicsock tests on Windows
These tests are passing locally and on CI. They had failed earlier in
the day when first fixing up CI, and it is not immediately clear why. I
have cycled IPv6 support locally, but this should not have a substantial
effect.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-14 22:53:53 -07:00
James Tucker
bd0c32ca21 tsnet: disable TestLoopbackSOCKS5 on Windows
This test is not regularly passing on CI, but seems to pass reliably
locally. Needs deeper debugging.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-14 22:28:04 -07:00
James Tucker
b7f51a1468 .github/workflows: add artifact caching and remove double build on race
Go artifact caching will help provided that the cache remains small
enough - we can reuse the strategy from the Windows build where we only
cache and pull the zips, but let go(1) do the many-file unpacking as it
does so faster.

The race matrix was building once without race, then running all the
tests with race, so change the matrix to incldue a `buildflags`
parameter and use that both in the build and test steps.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-14 22:07:06 -07:00
Andrew Dunham
f352f8a0e6 util/set: move Slice type from corp to oss
This is an exact copy of the files misc/set/set{,_test}.go from
tailscale/corp@a5415daa9c, plus the
license headers.

For use in #7877

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I712d09c6d1a180c6633abe3acf8feb59b27e2866
2023-04-14 19:25:39 -04:00
James Tucker
8dec1a8724 .github/workflows: reenable Windows CI, disable broken tests
We accidentally switched to ./tool/go in
4022796484 which resulted in no longer
running Windows builds, as this is attempting to run a bash script.

I was unable to quickly fix the various tests that have regressed, so
instead I've added skips referencing #7876, which we need to back and
fix.

Updates #7262
Updates #7876

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-14 14:13:53 -07:00
Will Norris
4ecc7fdf5f api.md: update example auth key value
example was missing the "-auth" type in the key prefix, which all new
keys now contain.  Also update key ID to match the full key, and fix
indenting of closing braces.

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-14 11:35:33 -07:00
Brad Fitzpatrick
6866aaeab3 wgengine/magicsock: factor out receiveIPv4 & receiveIPv6 common code
Updates #2331

Change-Id: I801df38b217f5d17203e8dc3b8654f44747e0f4b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-14 10:40:42 -07:00
Brad Fitzpatrick
c889254b42 net/art: skip tests on CI for now
To get the tree green again for other people.

Updates #7866

Change-Id: Ibdad2e1408e5f0c97e49a148bfd77aad17c2c5e5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-14 10:24:38 -07:00
Andrew Dunham
228d0c6aea net/netcheck: use dnscache.Resolver when resolving DERP IPs
This also adds a bunch of tests for this function to ensure that we're
returning the proper IP(s) in all cases.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0d9d57170dbab5f2bf07abdf78ecd17e0e635399
2023-04-14 13:14:29 -04:00
Maisem Ali
64bbf1738e tailcfg: make SelfNodeV4MasqAddrForThisPeer a pointer
This makes `omitempty` actually work, and saves bytes in each map response.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-13 11:28:33 -07:00
David Anderson
a5fd51ebdc net/art: disable the IPv6 100k routes benchmark.
At the current unoptimized memory utilization of the various data structures,
100k IPv6 routes consumes in the ballpark of 3-4GiB, which risks OOMing our
386 test machine.

Until we have the optimizations to (drastically) reduce that consumption,
skip the test that bloats too much for 32-bit machines.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-04-13 09:04:17 -07:00
David Anderson
a7c910e361 net/art: implement the Table type, a multi-level art route table.
Updates #7781

                           │    sec/op     │
TableInsertion/ipv4/10       1.562µ ±   2%
TableInsertion/ipv4/100      2.398µ ±   5%
TableInsertion/ipv4/1000     2.097µ ±   3%
TableInsertion/ipv4/10000    2.756µ ±   4%
TableInsertion/ipv4/100000   2.473µ ±  13%
TableInsertion/ipv6/10       7.649µ ±   2%
TableInsertion/ipv6/100      12.09µ ±   3%
TableInsertion/ipv6/1000     14.84µ ±   5%
TableInsertion/ipv6/10000    14.72µ ±   8%
TableInsertion/ipv6/100000   13.23µ ±  41%
TableDelete/ipv4/10          378.4n ±   5%
TableDelete/ipv4/100         366.9n ±   3%
TableDelete/ipv4/1000        418.6n ±   3%
TableDelete/ipv4/10000       609.2n ±  11%
TableDelete/ipv4/100000      679.2n ±  28%
TableDelete/ipv6/10          504.2n ±   4%
TableDelete/ipv6/100         959.5n ±  12%
TableDelete/ipv6/1000        1.436µ ±   6%
TableDelete/ipv6/10000       1.772µ ±  15%
TableDelete/ipv6/100000      1.172µ ± 113%
TableGet/ipv4/10             32.14n ±  11%
TableGet/ipv4/100            38.58n ±   2%
TableGet/ipv4/1000           45.03n ±   2%
TableGet/ipv4/10000          52.90n ±   7%
TableGet/ipv4/100000         135.2n ±  11%
TableGet/ipv6/10             41.55n ±   1%
TableGet/ipv6/100            44.78n ±   2%
TableGet/ipv6/1000           49.03n ±   2%
TableGet/ipv6/10000          65.38n ±   5%
TableGet/ipv6/100000         525.0n ±  39%

                           │   avg-B/op   │
TableInsertion/ipv4/10       25.18Ki ± 0%
TableInsertion/ipv4/100      17.63Ki ± 0%
TableInsertion/ipv4/1000     14.14Ki ± 0%
TableInsertion/ipv4/10000    12.92Ki ± 0%
TableInsertion/ipv4/100000   11.13Ki ± 0%
TableInsertion/ipv6/10       76.87Ki ± 0%
TableInsertion/ipv6/100      98.33Ki ± 0%
TableInsertion/ipv6/1000     91.44Ki ± 0%
TableInsertion/ipv6/10000    90.39Ki ± 0%
TableInsertion/ipv6/100000   87.19Ki ± 0%
TableDelete/ipv4/10            3.230 ± 0%
TableDelete/ipv4/100           4.020 ± 0%
TableDelete/ipv4/1000          3.990 ± 0%
TableDelete/ipv4/10000         4.000 ± 0%
TableDelete/ipv4/100000        4.000 ± 0%
TableDelete/ipv6/10            16.00 ± 0%
TableDelete/ipv6/100           16.00 ± 0%
TableDelete/ipv6/1000          16.00 ± 0%
TableDelete/ipv6/10000         16.00 ± 0%
TableDelete/ipv6/100000        16.00 ± 0%

                           │ avg-allocs/op │
TableInsertion/ipv4/10          2.900 ± 0%
TableInsertion/ipv4/100         2.330 ± 0%
TableInsertion/ipv4/1000        2.070 ± 0%
TableInsertion/ipv4/10000       1.980 ± 0%
TableInsertion/ipv4/100000      1.840 ± 0%
TableInsertion/ipv6/10          6.800 ± 0%
TableInsertion/ipv6/100         8.420 ± 0%
TableInsertion/ipv6/1000        7.900 ± 0%
TableInsertion/ipv6/10000       7.820 ± 0%
TableInsertion/ipv6/100000      7.580 ± 0%
TableDelete/ipv4/10             1.000 ± 0%
TableDelete/ipv4/100            1.000 ± 0%
TableDelete/ipv4/1000           1.000 ± 0%
TableDelete/ipv4/10000          1.000 ± 0%
TableDelete/ipv4/100000         1.000 ± 0%
TableDelete/ipv6/10             1.000 ± 0%
TableDelete/ipv6/100            1.000 ± 0%
TableDelete/ipv6/1000           1.000 ± 0%
TableDelete/ipv6/10000          1.000 ± 0%
TableDelete/ipv6/100000         1.000 ± 0%

                           │   routes/s   │
TableInsertion/ipv4/10       640.3k ±  2%
TableInsertion/ipv4/100      417.1k ±  5%
TableInsertion/ipv4/1000     477.0k ±  3%
TableInsertion/ipv4/10000    362.8k ±  5%
TableInsertion/ipv4/100000   404.5k ± 15%
TableInsertion/ipv6/10       130.7k ±  1%
TableInsertion/ipv6/100      82.69k ±  3%
TableInsertion/ipv6/1000     67.37k ±  5%
TableInsertion/ipv6/10000    67.93k ±  9%
TableInsertion/ipv6/100000   75.63k ± 29%
TableDelete/ipv4/10          2.642M ±  6%
TableDelete/ipv4/100         2.726M ±  3%
TableDelete/ipv4/1000        2.389M ±  3%
TableDelete/ipv4/10000       1.641M ± 12%
TableDelete/ipv4/100000      1.472M ± 27%
TableDelete/ipv6/10          1.984M ±  4%
TableDelete/ipv6/100         1.042M ± 11%
TableDelete/ipv6/1000        696.5k ±  6%
TableDelete/ipv6/10000       564.4k ± 13%
TableDelete/ipv6/100000      853.6k ± 53%

                     │   addrs/s    │
TableGet/ipv4/10       31.11M ± 10%
TableGet/ipv4/100      25.92M ±  2%
TableGet/ipv4/1000     22.21M ±  2%
TableGet/ipv4/10000    18.91M ±  8%
TableGet/ipv4/100000   7.397M ± 12%
TableGet/ipv6/10       24.07M ±  1%
TableGet/ipv6/100      22.33M ±  2%
TableGet/ipv6/1000     20.40M ±  2%
TableGet/ipv6/10000    15.30M ±  5%
TableGet/ipv6/100000   1.905M ± 28%

                     │    B/op    │
TableGet/ipv4/10       4.000 ± 0%
TableGet/ipv4/100      4.000 ± 0%
TableGet/ipv4/1000     4.000 ± 0%
TableGet/ipv4/10000    4.000 ± 0%
TableGet/ipv4/100000   4.000 ± 0%
TableGet/ipv6/10       16.00 ± 0%
TableGet/ipv6/100      16.00 ± 0%
TableGet/ipv6/1000     16.00 ± 0%
TableGet/ipv6/10000    16.00 ± 0%
TableGet/ipv6/100000   16.00 ± 0%

                     │ allocs/op  │
TableGet/ipv4/10       1.000 ± 0%
TableGet/ipv4/100      1.000 ± 0%
TableGet/ipv4/1000     1.000 ± 0%
TableGet/ipv4/10000    1.000 ± 0%
TableGet/ipv4/100000   1.000 ± 0%
TableGet/ipv6/10       1.000 ± 0%
TableGet/ipv6/100      1.000 ± 0%
TableGet/ipv6/1000     1.000 ± 0%
TableGet/ipv6/10000    1.000 ± 0%
TableGet/ipv6/100000   1.000 ± 0%

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-04-13 09:04:17 -07:00
Mihai Parparita
edb02b63f8 net/sockstats: pass in logger to sockstats.WithSockStats
Using log.Printf may end up being printed out to the console, which
is not desirable. I noticed this when I was investigating some client
logs with `sockstats: trace "NetcheckClient" was overwritten by another`.
That turns to be harmless/expected (the netcheck client will fall back
to the DERP client in some cases, which does its own sockstats trace).

However, the log output could be visible to users if running the
`tailscale netcheck` CLI command, which would be needlessly confusing.

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-12 18:40:03 -07:00
Mihai Parparita
782ccb5655 .github/workflows: run one set of tests with the tailscale_go build tag
We use it to gate code that depends on custom Go toolchain, but it's
currently only passed in the corp runners. Add a set on OSS so that we
can catch regressions earlier.

To specifically test sockstats this required adding a build tag to
explicitly enable them -- they're normally on for iOS, macOS and Android
only, and we don't run tests on those platforms normally.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-12 16:34:26 -07:00
Will Norris
bb34589748 log/sockstatlog: limit sockstat logs to 5 MB
Updates tailscale/corp#9230

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-12 15:32:01 -07:00
Brad Fitzpatrick
9e50da321b client/tailscale: dial LocalAPI at 127.0.0.1 on macOS
Updates #7851

Change-Id: Ib53cf53cdfee277ef42f7833352bc51ecb5db959
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-12 14:09:24 -07:00
Mihai Parparita
bdc7a61c24 tool/gocross: add ts_macext build tag for Xcode builds
It's used to control various opt-in functionality for the macOS and iOS
apps, and was lost in the migration to gocross.

Updates tailscale/tailscale#7769

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-11 11:27:26 -07:00
License Updater
33b006cacf licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-11 09:06:08 -07:00
License Updater
e5d272f445 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-04-11 09:05:56 -07:00
Flakes Updater
7c95734907 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-04-11 08:54:53 -07:00
Anton Tolchanov
8546ff98fb tsweb: move varz handler(s) into separate modules
This splits Prometheus metric handlers exposed by tsweb into two
modules:
- `varz.Handler` exposes Prometheus metrics generated by our expvar
  converter;
- `promvarz.Handler` combines our expvar-converted metrics and native
  Prometheus metrics.

By default, tsweb will use the promvarz handler, however users can keep
using only the expvar converter. Specifically, `tailscaled` now uses
`varz.Handler` explicitly, which avoids a dependency on the
(heavyweight) Prometheus client.

Updates https://github.com/tailscale/corp/issues/10205

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-04-11 08:37:32 +01:00
Anton Tolchanov
c153e6ae2f prober: migrate to Prometheus metric library
This provides an example of using native Prometheus metrics with tsweb.

Prober library seems to be the only user of PrometheusVar, so I am
removing support for it in tsweb.

Updates https://github.com/tailscale/corp/issues/10205

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-04-11 08:37:32 +01:00
Anton Tolchanov
11e6247d2a tsweb: expose native Prometheus metrics in /debug/varz
The handler will expose built-in process and Go metrics by default,
which currently duplicate some of the expvar-proxied metrics
(`goroutines` vs `go_goroutines`, `memstats` vs `go_memstats`), but as
long as their names are different, Prometheus server will just scrape
both.

This will change /debug/varz behaviour for most tsweb binaries, but
notably not for control, which configures a `tsweb.VarzHandler`
[explicitly](a5b5d5167f/cmd/tailcontrol/tailcontrol.go (L779))

Updates https://github.com/tailscale/corp/issues/10205

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-04-11 08:37:32 +01:00
License Updater
690446c784 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-10 21:29:19 -07:00
Mihai Parparita
cef0a474f8 ipn/ipnlocal: check that sockstatLogger is available in c2n endpoint
Otherwise there may be a panic if it's nil (and the control side of
the c2n call will just time out).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-10 17:59:26 -07:00
Mihai Parparita
03b2c44a21 ipn/ipnlocal: more explicitly say if sockstats are not available
Makes it more apparent in the PeerAPI endpoint that the client was
not built with the appropriate toolchain or build tags.

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-10 14:59:11 -07:00
Flakes Updater
1bec2cbbd5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-04-10 10:34:42 -07:00
Jordan Whited
f571536598 go.mod: bump wireguard-go (#7836)
This pulls in a synchronization optimization, see
tailscale/wireguard-go@af17262.

Updates tailscale/corp#8734

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-04-10 10:32:46 -07:00
James Tucker
e09c434e5d wgengine/magicsock: remove locking sync requirements on conn disco keys
The lazy initialization of the disco key is not necessary, and
contributes to unnecessary locking and state checking.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-10 09:47:54 -07:00
James Tucker
e1b71c83ac wgengine/magicsock: remove unused fields on discoInfo
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-10 09:24:00 -07:00
James Tucker
a257b2f88b wgengine/magicsock: add immutability documentation to endpointDisco
Updates #7825

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-10 09:13:44 -07:00
James Tucker
fb18af5564 wgengine/netstack: fix data-race on startup
Running tailscaled with the race detector enabled immediately fires on
this field, as it is updated after first read.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-10 09:13:21 -07:00
Charlotte Brandhorst-Satzkorn
c573bef0aa tailcfg,wgengine: add initial support for WireGuard only peers
A peer can have IsWireGuardOnly, which means it will not support DERP or
Disco, and it must have Endpoints filled in order to be usable.

In the present implementation only the first Endpoint will be used as
the bestAddr.

Updates tailscale/corp#10351

Co-authored-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
2023-04-08 22:08:25 -07:00
James Tucker
6cfcb3cae4 wgengine/magicsock: fix synchronization of endpoint disco fields
Identified in review in #7821 endpoint.discoKey and endpoint.discoShort
are often accessed without first taking endpoint.mu. The arrangement
with endpoint.mu is inconvenient for a good number of those call-sites,
so it is instead replaced with an atomic pointer to carry both pieces of
disco info. This will also help with #7821 that wants to add explicit
checks/guards to disable disco behaviors when disco keys are missing
which is necessarily implicitly mostly covered by this change.

Updates #7821

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-08 17:15:54 -07:00
Mihai Parparita
e978299bf0 net/sockstats: disable deltas for the cell radio power state metric
Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-07 18:01:01 -07:00
Will Norris
22680a11ae net/sockstats: return early if no radio period length
Signed-off-by: Will Norris <will@tailscale.com>
2023-04-07 17:03:36 -07:00
Will Norris
75784e10e2 sockstats: add client metrics for radio power state
power state is very roughly approximated based on observed network
activity and AT&T's state transition timings for a typical 3G radio.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-07 14:38:27 -07:00
Tom DNetto
6a627e5a33 net, wgengine/capture: encode NAT addresses in pcap stream
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-04-06 13:26:00 -07:00
License Updater
92459a9248 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-04-06 11:31:00 -07:00
License Updater
7012bf7981 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-06 11:30:46 -07:00
License Updater
07b29f13dc licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-06 11:30:16 -07:00
Mihai Parparita
f49b9f75b8 util/clientmetric: allow client metric values to be provided by a function
Adds NewGaugeFunc and NewCounterFunc (inspired by expvar.Func) which
change the current value to be reported by a function. This allows
some client metric values to be computed on-demand during uploading (at
most every 15 seconds), instead of being continuously updated.

clientmetric uploading had a bunch of micro-optimizations for memory
access (#3331) which are not possible with this approach. However, any
performance hit from function-based metrics is contained to those metrics
only, and we expect to have very few.

Also adds a DisableDeltas() option for client metrics, so that absolute
values are always reported. This makes server-side processing of some
metrics easier to reason about.

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-05 17:21:16 -07:00
phirework
c0e0a5458f cmd/tailscale: show reauth etc. links even if no login name (#7803)
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2023-04-05 17:00:18 -04:00
shayne
81fd00a6b7 cmd/tailscale/cli: [serve] add support for proxy paths (#7800) 2023-04-05 12:33:18 -04:00
Maisem Ali
d42d570066 ssh/tailssh: handle output matching better in tests (#7799) 2023-04-05 11:35:02 -04:00
Brad Fitzpatrick
2c0bda6e2e ssh/tailssh: make Tailscale SSH work on gokrazy
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-04 22:22:02 -07:00
Flakes Updater
3d29da105c go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-04-04 20:01:56 -07:00
Jordan Whited
765d3253f3 go.mod: bump wireguard-go (#7792)
Pull in TUN checksum optimizations and crypto channel changes.

Updates tailscale/corp#8734

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-04-04 19:56:00 -07:00
shayne
ba4e58f429 cmd/tailscale/cli: do not allow turning Funnel on while shields-up (#7770) 2023-04-04 22:20:27 -04:00
valscale
7bfb7744b7 derp,magicsock: add debug envknobs for HTTP and derp server name (#7744)
Make developing derp easier by:

1. Creating an envknob telling clients to use HTTP to connect to derp
servers, so devs don't have to acquire a valid TLS cert.

2. Creating an envknob telling clients which derp server to connect
to, so devs don't have to edit the ACLs in the admin console to add a
custom DERP map.

3. Explaining how the -dev and -a command lines args to derper
interact.

To use this:

1. Run derper with -dev.

2. Run tailscaled with TS_DEBUG_USE_DERP_HTTP=1 and
TS_DEBUG_USE_DERP_ADDR=localhost

This will result in the client connecting to derp via HTTP on port
3340.

Fixes #7700

Signed-off-by: Val <valerie@tailscale.com>
2023-04-04 17:10:50 -07:00
Jordan Whited
f475e5550c net/neterror, wgengine/magicsock: use UDP GSO and GRO on Linux (#7791)
This commit implements UDP offloading for Linux. GSO size is passed to
and from the kernel via socket control messages. Support is probed at
runtime.

UDP GSO is dependent on checksum offload support on the egress netdev.
UDP GSO will be disabled in the event sendmmsg() returns EIO, which is
a strong signal that the egress netdev does not support checksum
offload.

Updates tailscale/corp#8734

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-04-04 16:32:16 -07:00
David Anderson
45138fcfba go.toolchain.rev: update for go 1.20.3
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-04-04 11:44:02 -07:00
James Tucker
b0ed863d55 atomicfile: use /tmp for socket path on macOS
macOS does not allow unix socket creation in private temp directories,
but global /tmp is ok, so swap out for global temp for now.

Updates #7658
Updates #7785

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-04 09:14:37 -07:00
David Anderson
4d1b3bc26f net/art: implement the stride table building block of ART
A stride table is an 8-bit routing table implemented as an array binary
tree, with a special tree updating function (allot) that enables lightning
fast address lookups and reasonably fast insertion and deletion.

Insertion, deletion and lookup are all allocation-free.

Updates #7781

                                        │    sec/op    │
StrideTableInsertion/10/random_order       16.79n ± 2%
StrideTableInsertion/10/largest_first      16.83n ± 1%
StrideTableInsertion/10/smallest_first     16.83n ± 0%
StrideTableInsertion/50/random_order       17.84n ± 1%
StrideTableInsertion/50/largest_first      20.04n ± 1%
StrideTableInsertion/50/smallest_first     16.39n ± 0%
StrideTableInsertion/100/random_order      14.63n ± 0%
StrideTableInsertion/100/largest_first     17.45n ± 4%
StrideTableInsertion/100/smallest_first    12.98n ± 0%
StrideTableInsertion/200/random_order      12.51n ± 4%
StrideTableInsertion/200/largest_first     18.36n ± 3%
StrideTableInsertion/200/smallest_first    9.609n ± 3%
StrideTableDeletion/10/random_order        19.50n ± 1%
StrideTableDeletion/10/largest_first       19.34n ± 0%
StrideTableDeletion/10/smallest_first      19.43n ± 0%
StrideTableDeletion/50/random_order        14.58n ± 1%
StrideTableDeletion/50/largest_first       14.27n ± 2%
StrideTableDeletion/50/smallest_first      15.51n ± 0%
StrideTableDeletion/100/random_order       12.02n ± 3%
StrideTableDeletion/100/largest_first      10.64n ± 0%
StrideTableDeletion/100/smallest_first     13.21n ± 3%
StrideTableDeletion/200/random_order       14.05n ± 4%
StrideTableDeletion/200/largest_first      9.288n ± 5%
StrideTableDeletion/200/smallest_first     18.51n ± 1%
StrideTableGet                            0.5010n ± 0%

                                        │  routes/s   │
StrideTableInsertion/10/random_order      59.55M ± 2%
StrideTableInsertion/10/largest_first     59.42M ± 1%
StrideTableInsertion/10/smallest_first    59.43M ± 0%
StrideTableInsertion/50/random_order      56.04M ± 1%
StrideTableInsertion/50/largest_first     49.91M ± 1%
StrideTableInsertion/50/smallest_first    61.00M ± 0%
StrideTableInsertion/100/random_order     68.35M ± 0%
StrideTableInsertion/100/largest_first    57.32M ± 3%
StrideTableInsertion/100/smallest_first   77.06M ± 0%
StrideTableInsertion/200/random_order     79.93M ± 4%
StrideTableInsertion/200/largest_first    54.47M ± 3%
StrideTableInsertion/200/smallest_first   104.1M ± 3%
StrideTableDeletion/10/random_order       51.28M ± 1%
StrideTableDeletion/10/largest_first      51.70M ± 0%
StrideTableDeletion/10/smallest_first     51.48M ± 0%
StrideTableDeletion/50/random_order       68.60M ± 1%
StrideTableDeletion/50/largest_first      70.09M ± 2%
StrideTableDeletion/50/smallest_first     64.45M ± 0%
StrideTableDeletion/100/random_order      83.21M ± 3%
StrideTableDeletion/100/largest_first     94.03M ± 0%
StrideTableDeletion/100/smallest_first    75.69M ± 3%
StrideTableDeletion/200/random_order      71.20M ± 5%
StrideTableDeletion/200/largest_first     107.7M ± 5%
StrideTableDeletion/200/smallest_first    54.02M ± 1%
StrideTableGet                            1.996G ± 0%

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-04-04 09:00:24 -07:00
Will Norris
6d5c3c1637 ipn: prefer allow/denylist terminology
Signed-off-by: Will Norris <will@tailscale.com>
2023-04-04 08:02:50 -07:00
Will Norris
5a3da3cd7f ipn: add sockstat logger to stable builds
This makes the sockstat logger available on all builds, but only enables
it by default for unstable.  For stable builds, the logger must be
explicitly enabled via C2N component logger.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Will Norris <will@tailscale.com>
2023-04-03 14:44:12 -07:00
Aaron Klotz
90fd04cbde ipn/ipnlocal, util/winutil/policy: modify Windows profile migration to load legacy prefs from within tailscaled
I realized that a lot of the problems that we're seeing around migration and
LocalBackend state can be avoided if we drive Windows pref migration entirely
from within tailscaled. By doing it this way, tailscaled can automatically
perform the migration as soon as the connection with the client frontend is
established.

Since tailscaled is already running as LocalSystem, it already has access to
the user's local AppData directory. The profile manager already knows which
user is connected, so we simply need to resolve the user's prefs file and read
it from there.

Of course, to properly migrate this information we need to also check system
policies. I moved a bunch of policy resolution code out of the GUI and into
a new package in util/winutil/policy.

Updates #7626

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-04-03 14:41:46 -07:00
Mihai Parparita
e3cb8cc88d ipn/ipnlocal: automatically upload sockstats logs when the period ends
Avoids needing a separate c2n call to get the logs uploaded.

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-03 14:31:15 -07:00
Andrew Dunham
8d3acc9235 util/sysresources, magicsock: scale DERP buffer based on system memory
This adds the util/sysresources package, which currently only contains a
function to return the total memory size of the current system.

Then, we modify magicsock to scale the number of buffered DERP messages
based on the system's available memory, ensuring that we never use a
value lower than the previous constant of 32.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib763c877de4d0d4ee88869078e7d512f6a3a148d
2023-04-03 17:14:14 -04:00
Tom DNetto
483109b8fc client/tailscale: Fix NPE caused by erroneous close in error case
Fixes https://github.com/tailscale/tailscale/issues/7572

When handling an error during `StreamDebugCapture`, the response body
is closed, even though the response struct is always nil. Thanks
to https://github.com/darkrain42 for debugging this!!

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-04-03 11:47:27 -07:00
shayne
59879e5770 cmd/tailscale/cli: make serve and funnel visible in list (#7737) 2023-04-03 10:09:04 -04:00
Flakes Updater
1bf65e4760 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-04-02 21:41:41 -07:00
Mihai Parparita
38bbb30aaf .github/workflows: remove tsconnect-pkg-publish.yml
Moved to the corp repo with tailscale/corp#7990

Updates tailscale/corp#10165

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-31 14:43:59 -07:00
James Tucker
f4da995940 atomicfile: reject overwriting irregular files
The intent of atomicfile is to overwrite regular files. Most use cases
that would overwrite irregular files, unix sockets, named pipes,
devices, and so on are more than likely misuse, so disallow them.

Fixes #7658

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-31 13:29:52 -07:00
Mihai Parparita
02582083d5 cmd/tsconnect: allow root directory to be passed in
#7339 changed the root directory logic to find the ancestor of the cwd
with a go.mod file. This works when running the the binary from this
repo directly, but breaks when we're a dependency in another repo.

Allow the directory to be passed in via a -rootdir flag (the repo that
depends on it can then use `go list -m -f '{{.Dir}}' tailscale.com`
or similar to pass in the value).

Updates tailscale/corp#10165

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-30 16:23:55 -07:00
James Tucker
40fa2a420c envknob,net/tstun,wgengine: use TS_DEBUG_MTU consistently
Noted on #5915 TS_DEBUG_MTU was not used consistently everywhere.
Extract the default into a function that can apply this centrally and
use it everywhere.

Added envknob.Lookup{Int,Uint}Sized to make it easier to keep CodeQL
happy when using converted values.

Updates #5915

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-30 14:26:10 -07:00
Andrew Dunham
8ed4fd1dbc envknob/logknob: add package for configurable logging
A LogKnob allows enabling logs with an envknob, netmap capability, and
manually, and calling a logging function when logs are enabled.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id66c608d4e488bfd4eaa5e867a8d9289686748be
2023-03-30 11:16:26 -04:00
Andrew Dunham
3b39ca9017 ipn/ipnlocal: update comment in SetComponentDebugLogging
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8c36a62079dce77fc81b9cdfb5fe723b007218ba
2023-03-30 10:13:21 -04:00
Maisem Ali
e0d291ab8a ipn/store: add support for stores to hook into a custom dialer
For stores like k8s secrets we need to dial out to the k8s API as though Tailscale
wasn't running. The issue currently only manifests when you try to use an exit node
while running inside a k8s cluster and are trying to use Kubernetes secrets as the
backing store.

This doesn't address cmd/containerboot, which I'll do in a follow up.

Updates #7695

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-29 16:35:46 -07:00
License Updater
2b00d6922f licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-03-29 16:12:48 -07:00
License Updater
7b4e85aa78 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-03-29 15:16:11 -07:00
Will Norris
e99c7c3ee5 sockstats: add labels for netlog and sockstatlog packages
Signed-off-by: Will Norris <will@tailscale.com>
2023-03-29 14:53:07 -07:00
Andrew Dunham
38e4d303a2 net/tshttpproxy: don't proxy through ourselves
When running a SOCKS or HTTP proxy, configure the tshttpproxy package to
drop those addresses from any HTTP_PROXY or HTTPS_PROXY environment
variables.

Fixes #7407

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6cd7cad7a609c639780484bad521c7514841764b
2023-03-29 17:09:45 -04:00
Will Norris
62a1e9a44f log/sockstatlog: add delay before writing logs to disk
Split apart polling of sockstats and logging them to disk.  Add a 3
second delay before writing logs to disk to prevent an infinite upload
loop when uploading stats to logcatcher.

Fixes #7719

Signed-off-by: Will Norris <will@tailscale.com>
2023-03-29 13:10:42 -07:00
Maisem Ali
985535aebc net/tstun,wgengine/*: add support for NAT to routes
This adds support to make exit nodes and subnet routers work
when in scenarios where NAT is required.

It also updates the NATConfig to be generated from a `wgcfg.Config` as
that handles merging prefs with the netmap, so it has the required information
about whether an exit node is already configured and whether routes are accepted.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-29 12:30:18 -07:00
Maisem Ali
d1d5d52b2c net/tstun/table: add initial RoutingTable implementation
It is based on `*tempfork/device.AllowedIPs`.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-29 12:30:18 -07:00
Maisem Ali
2522b0615f tempfork/device: add a temp fork of golang.zx2c4.com/wireguard/device
This will allow us to reuse the AllowedIPs for NAT decisions in a follow on commit.

The files `allowedips_*.go` are as-is, `peer.go` only keeps the `Peer` declaration with a
single element required for AllowedIPs.

Upstream commit https://git.zx2c4.com/wireguard-go/commit/?id=052af4a8072bbbd3bfe7edf46fe3c1b350f71f08

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-29 09:53:09 -07:00
Andrew Dunham
c98652c333 doctor/permissions: add new check to print process permissions
Since users can run tailscaled in a variety of ways (root, non-root,
non-root with process capabilities on Linux), this check will print the
current process permissions to the log to aid in debugging.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ida93a206123f98271a0c664775d0baba98b330c7
2023-03-29 11:50:23 -04:00
License Updater
524f53de89 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-03-28 20:34:14 -07:00
James Tucker
8c2b755b2e tool/gocross: use grep -E over egrep to avoid warning
Recent egrep builds produce a warning:

```
egrep: warning: egrep is obsolescent; using grep -E
```

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-28 20:07:11 -07:00
James Tucker
a31e43f760 go.mod: bump gvisor to 20230320 for dispatcher locking
Upstream improved code around an issue showing up in CI, where sometimes
shutdown will race on endpoint.dispatcher being nil'd, causing a panic
down stack of injectInbound. The upstream patch makes some usage more
safe, but it does not itself fix the local issue.

See panic in https://github.com/tailscale/tailscale/actions/runs/4548299564/jobs/8019187385#step:7:843

See fix in google/gvisor@13d7bf69d8

Updates #7715

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-28 20:06:54 -07:00
James Tucker
c628132b34 wgengine/netstack: do not send packets to netstack after close
Use the local context on Impl to check for shut down state in order to
drop rather than inject packets after close has begun.

Netstack sets endpoint.dispatcher to nil during shutdown. After the
recent adjustment in 920ec69241 we now
wait for netstack to fully shutdown before we release tests. This means
that we may continue to accept packets and attempt to inject them, which
we must prevent in order to avoid nil pointer panic.

References google/gvisor#8765
Fixes #7715

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-28 19:55:45 -07:00
Maisem Ali
e04acabfde ssh/tailssh: fix race in errors returned when starting recorder
There were two code paths that could fail depending on how fast
the recorder responses. This fixes that by returning the correct
error from both paths.

Fixes #7707

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-28 19:15:46 -07:00
Flakes Updater
cb960d6cdd go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-03-28 15:39:08 -07:00
Jordan Whited
27e37cf9b3 go.mod, net/tstun, wgengine/magicsock: update wireguard-go (#7712)
This commit updates the wireguard-go dependency to pull in fixes for
the tun package, specifically 052af4a and aad7fca.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-03-28 15:37:11 -07:00
License Updater
946451b43e licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-03-28 09:06:37 -07:00
License Updater
840d69e1eb licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-03-28 08:48:55 -07:00
Josh Bleecher Snyder
3ba9f8dd04 util/codegen: add -copyright to control presence of copyright headers
Fixes #7702

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2023-03-27 16:33:34 -07:00
Will Norris
7c99210e68 log: allow toggling sockstat logs via c2n component logging
Signed-off-by: Will Norris <will@tailscale.com>
2023-03-27 15:44:47 -07:00
Maisem Ali
920ec69241 tsnet,wgenegine/netstack: add test and fix resource leaks
We were not closing the http.Server and were also not waiting for
netstack to fully close.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-27 12:13:12 -07:00
Anton Tolchanov
2a933c1903 cmd/tailscale: extend hostname validation (#7678)
In addition to checking the total hostname length, validate characters used in each DNS label and label length.

Updates https://github.com/tailscale/corp/issues/10012

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-27 18:21:58 +01:00
shayne
43f7ec48ca funnel: change references from alpha to beta (#7613)
Updates CLI and docs to reference Funnel as beta

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-27 10:12:32 -04:00
shayne
3177ccabe5 ipn/ipnlocal: [serve/funnel] use actual SrcAddr as X-Forwarded-For (#7600)
The reverse proxy was sending the ingressd IPv6 down as the
X-Forwarded-For. This update uses the actual remote addr.

Updates tailscale/corp#9914

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-27 10:12:04 -04:00
shayne
7908b6d616 ipn/ipnlocal: [serve] Trim mountPoint prefix from proxy path (#7334)
This change trims the mountPoint from the request URL path before
sending the request to the reverse proxy.

Today if you mount a proxy at `/foo` and request to
`/foo/bar/baz`, we leak the `mountPoint` `/foo` as part of the request
URL's path.

This fix makes removed the `mountPoint` prefix from the path so
proxied services receive requests as if they were running at the root
(`/`) path.

This could be an issue if the app generates URLs (in HTML or otherwise)
and assumes `/path`. In this case, those URLs will 404.

With that, I still think we should trim by default and not leak the
`mountPoint` (specific to Tailscale) into whatever app is hosted.
If it causes an issue with URL generation, I'd suggest looking at configuring
an app-specific path prefix or running Caddy as a more advanced
solution.

Fixes: #6571

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-27 10:11:46 -04:00
Denton Gentry
ed10a1769b scripts/installer.sh: check Photon OS version with pkg server.
Photon OS support crossed streams with using pkgserve to check
for supported versions 6f9aed1656.
Make Photon OS also rely on pkgserve.

Updates https://github.com/tailscale/tailscale/issues/7651
Updates https://github.com/tailscale/corp/issues/8952

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-25 18:08:20 -07:00
Maisem Ali
5ba57e4661 ssh/tailssh: add tests for recording failure
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-25 11:15:42 -07:00
Denton Gentry
d5abdd915e scripts/installer: add VMWare PhotonOS.
Fixes https://github.com/tailscale/tailscale/issues/7651

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-25 04:53:40 -07:00
valscale
74eb99aed1 derp, derphttp, magicsock: send new unknown peer frame when destination is unknown (#7552)
* wgengine/magicsock: add envknob to send CallMeMaybe to non-existent peer

For testing older client version responses to the PeerGone packet format change.

Updates #4326

Signed-off-by: Val <valerie@tailscale.com>

* derp: remove dead sclient struct member replaceLimiter

Leftover from an previous solution to the duplicate client problem.

Updates #2751

Signed-off-by: Val <valerie@tailscale.com>

* derp, derp/derphttp, wgengine/magicsock: add new PeerGone message type Not Here

Extend the PeerGone message type by adding a reason byte. Send a
PeerGone "Not Here" message when an endpoint sends a disco message to
a peer that this server has no record of.

Fixes #4326

Signed-off-by: Val <valerie@tailscale.com>

---------

Signed-off-by: Val <valerie@tailscale.com>
2023-03-24 19:11:48 -07:00
Maisem Ali
09d0b632d4 ssh/tailssh: add session recording test for non-pty sessions
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 16:27:11 -07:00
Maisem Ali
d39a5e4417 tsnet: support TS_AUTH_KEY variant too
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 13:24:10 -07:00
Maisem Ali
d2fd101eb4 net/tstun: only log natConfig on changes
Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 13:23:49 -07:00
Maisem Ali
8ac5976897 logpolicy: do not upload logs in tests
Fixes tailscale/corp#10030

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 13:13:36 -07:00
Maisem Ali
7300b908fb logpolicy: split out DialContext into a func
Updates tailscale/corp#10030

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 13:13:36 -07:00
Maisem Ali
ca19cf13e9 log/sockstatlog: add resource cleanup test
Updates tailscale/corp#10030

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-24 13:13:36 -07:00
Andrew Dunham
33b359642e net/dns: don't send on closed channel in resolvedManager
Fixes #7686

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ibffb05539ab876b12407d77dcf2201d467895981
2023-03-24 15:34:54 -04:00
Anton Tolchanov
6f9aed1656 scripts: use pkg server to determine supported deb/rpm distros
Fixes https://github.com/tailscale/corp/issues/8952

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-24 17:36:43 +00:00
Andrew Dunham
4cb1bfee44 net/netcheck: improve determinism in hairpinning test
If multiple Go channels have a value (or are closed), receiving from
them all in a select will nondeterministically return one of the two
arms. In this case, it's possible that the hairpin check timer will have
expired between when we start checking and before we check at all, but
the hairpin packet has already been received. In such cases, we'd
nondeterministically set report.HairPinning.

Instead, check if we have a value in our results channel first, then
select on the value and timeout channel after. Also, add a test that
catches this particular failure.

Fixes #1795

Change-Id: I842ab0bd38d66fabc6cabf2c2c1bb9bd32febf35
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2023-03-24 12:01:23 -04:00
Maisem Ali
4a89642f7f log/sockstatlog: make shutdown close idle connections
Updates tailscale/corp#10030

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 19:15:30 -07:00
Maisem Ali
9e81db50f6 ipn/ipnlocal: use atomicfile.WriteFile in certFileStore
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 17:35:44 -07:00
Maisem Ali
8a11f76a0d ipn/ipnlocal: fix cert storage in Kubernetes
We were checking against the wrong directory, instead if we
have a custom store configured just use that.

Fixes #7588
Fixes #7665

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 17:35:44 -07:00
Maisem Ali
ec90522a53 ipn/ipnlocal: also store ACME keys in the certStore
We were not storing the ACME keys in the state store, they would always
be stored on disk.

Updates #7588

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 17:35:44 -07:00
Maisem Ali
0e203e414f net/packet: add checksum update tests
Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
0bf8c8e710 net/tstun: use p.Buffer() in more places
Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
f6ea6863de tstest/integration: add ping test w/ masquerades
Updates tailscale/corp#8020

Co-authored-by: Melanie Warrick <warrick@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
bb31fd7d1c net/tstun: add inital support for NAT v4
This adds support in tstun to utitilize the SelfNodeV4MasqAddrForThisPeer and
perform the necessary modifications to the packet as it passes through tstun.

Currently this only handles ICMP, UDP and TCP traffic.
Subnet routers and Exit Nodes are also unsupported.

Updates tailscale/corp#8020

Co-authored-by: Melanie Warrick <warrick@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
535fad16f8 net/tstun: rename filterIn/filterOut methods to be more descriptive
Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
f61b306133 tailcfg: add Node.SelfNodeV4MasqAddrForThisPeer
This only adds the field, to be used in a future commit.

Updates tailscale/corp#8020

Co-authored-by: Melanie Warrick <warrick@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:54:12 -07:00
Maisem Ali
583e86b7df ssh/tailssh: handle session recording when running in userspace mode
Previously it would dial out using the http.DefaultClient, however that doesn't work
when tailscaled is running in userspace mode (e.g. when testing).

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:51:10 -07:00
Maisem Ali
df89b7de10 cmd/k8s-operator: disable HTTP/2 for the auth proxy
Kubernetes uses SPDY/3.1 which is incompatible with HTTP/2, disable it
in the transport and server.

Fixes #7645
Fixes #7646

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 16:46:41 -07:00
Maisem Ali
8a246487c2 ssh/tailssh: enable recording of non-pty sessions
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 13:25:43 -07:00
Maisem Ali
8765568373 ssh/tailssh: add docs to CastHeader fields
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 13:25:43 -07:00
Maisem Ali
9d8b7a7383 ipn/store/kubestore: handle "/" in ipn.StateKeys
Kubernetes doesn't allow slashes as keys in secrets, replace them with "__".

This shows up in the kubernetes-operator now that tsnet sets resets the ServeConfig
at startup.

Fixes #7662

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 12:33:47 -07:00
Will Norris
57a008a1e1 all: pass log IDs as the proper type rather than strings
This change focuses on the backend log ID, which is the mostly commonly
used in the client.  Tests which don't seem to make use of the log ID
just use the zero value.

Signed-off-by: Will Norris <will@tailscale.com>
2023-03-23 11:26:55 -07:00
Andrew Dunham
13377e6458 ssh/tailssh: always assert our final uid/gid
Move the assertions about our post-privilege-drop UID/GID out of the
conditional if statement and always run them; I haven't been able to
find a case where this would fail. Defensively add an envknob to disable
this feature, however, which we can remove after the 1.40 release.

Updates #7616

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iaec3dba9248131920204bd6c6d34bbc57a148185
2023-03-23 14:26:36 -04:00
Andrew Dunham
9de8287d47 ssh/tailssh: lock OS thread during incubator
This makes it less likely that we trip over bugs like golang/go#1435.

Updates #7616

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic28c03c3ad8ed5274a795c766b767fa876029f0e
2023-03-23 14:09:57 -04:00
Maisem Ali
c350cd1f06 ssh/tailssh: use background context for uploading recordings
Otherwise we see errors like
```
ssh-session(sess-20230322T005655-5562985593): recording: error sending recording to <addr>:80: Post "http://<addr>:80/record": context canceled
```

The ss.ctx is closed when the session closes, but we don't want to break the upload at that time. Instead we want to wait for the session to
close the writer when it finishes, which it is already doing.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-23 10:46:09 -07:00
Will Norris
f13b8bf0cf log: use logtail to log and upload sockstat logs
Switch to using logtail for logging sockstat logs. Always log locally
(on supported platforms), but disable automatic uploading.  Change
existing c2n sockstats request to trigger upload to log server and
return log ID.

Signed-off-by: Will Norris <will@tailscale.com>
2023-03-23 09:39:41 -07:00
Mihai Parparita
731688e5cc ipn/localapi: add endpoint for adding debug log entries
Allows the iOS and macOS apps to include their frontend logs when
generating bug reports (tailscale/corp#9982).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-22 11:10:06 -07:00
Anton Tolchanov
7083246409 prober: only record latency for successful probes
This will make it easier to track probe latency on a dashboard.

Updates https://github.com/tailscale/corp/issues/9916

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-22 09:24:20 +00:00
Maisem Ali
d92047cc30 ssh/tailssh: allow recorders to be configured on the first or final action
Currently we only send down recorders in first action, allow the final action
to replace them but not to drop them.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-21 16:38:39 -07:00
Maisem Ali
7a97e64ef0 ssh/tailssh: add more metadata to recording header
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-21 16:35:59 -07:00
Denton Gentry
cc3806056f scripts/installer.sh: Add Ubuntu Lunar Lobster 23.04.
pkgs.tailscale.com added support in January, need to
add it to the installer script.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-21 15:17:38 -07:00
Maisem Ali
916aa782af ssh/tailssh: stream SSH recordings to configured recorders
Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-21 15:06:30 -07:00
Tom DNetto
60cd4ac08d cmd/tailscale/cli: move tskey-wrap functionality under lock sign
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-21 14:01:25 -07:00
Charlotte Brandhorst-Satzkorn
1b78dc1f33 tailcfg: move recorders field from SSHRule to SSHAction
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-03-21 13:11:19 -07:00
Charlotte Brandhorst-Satzkorn
3efd83555f tailcfg: add recorders field to SSHRule struct
This change introduces the Recorders field to the SSHRule struct. The
field is used to store and define addresses where the ssh recorder is
located.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-03-21 12:38:23 -07:00
Sam Linville
812025a39c words: what?! a llama?! he's supposed to be dead! (#7623)
pull the lever, kronk

Signed-off-by: Sam Linville <samlinville@protonmail.com>
2023-03-21 13:03:20 -04:00
Andrew Dunham
39b289578e ssh/tailssh: make uid an int instead of uint64
Follow-up to #7615

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib4256bff276f6d5cf95838d8e39c87b3643bde37
2023-03-21 12:45:07 -04:00
David Anderson
c9a4dbe383 tool/gocross: correctly embed the git commit into gocross
Previously, the build ended up embedding an empty string, which made
the shell wrapper rebuild gocross on every invocation. This is still
reasonably fast, but fixing the bypass shaves 80% off gocross's overhead
when no rebuild is needed.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-20 22:35:44 -07:00
Brad Fitzpatrick
f11c270c6b go.toolchain.rev: bump Go toolchain
For tailscale/go#60

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-03-20 21:53:13 -07:00
Mihai Parparita
d2dec13392 net/sockstats: export cellular-only clientmetrics
Followup to #7518 to also export client metrics when the active interface
is cellular.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-20 17:02:39 -07:00
David Anderson
e7a78bc28f tool/gocross: support running from outside the repo dir
A bunch of us invoke tool/go from outside the repo that hosts gocross,
as a way of accessing our version-controlled toolchain. This removes
assumptions from gocross that it's being invoked within the repository
that contains its source code and toolchain configuration.

Fixes tailscale/corp#9627

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-20 15:01:58 -07:00
David Anderson
df02bb013a tool/gocross: fail if the toolchain revision isn't findable
This used to make sense, but after a refactor somewhere along the line
this results in trying to download from a malformed URL and generally
confusing failures.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-20 15:01:58 -07:00
Denton Gentry
ebc630c6c0 net/interfaces: also allow link-local for AzureAppServices.
In May 2021, Azure App Services used 172.16.x.x addresses:
```
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:10:01:03 brd ff:ff:ff:ff:ff:ff
    inet 172.16.1.3/24 brd 172.16.1.255 scope global eth0
       valid_lft forever preferred_lft forever
```

Now it uses link-local:
```
2: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 8a:30:1f:50:1d:23 brd ff:ff:ff:ff:ff:ff
    inet 169.254.129.3/24 brd 169.254.129.255 scope global eth0
       valid_lft forever preferred_lft forever
```

This is reasonable for them to choose to do, it just broke the handling in net/interfaces.

This PR proposes to:
1. Always allow link-local in LocalAddresses() if we have no better
   address available.
2. Continue to make isUsableV4() conditional on an environment we know
   requires it.

I don't love the idea of having to discover these environments one by
one, but I don't understand the consequences of making isUsableV4()
return true unconditionally. It makes isUsableV4() essentially always
return true and perform no function.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-20 13:40:38 -07:00
Andrew Dunham
ccace1f7df ssh/tailssh: fix privilege dropping on FreeBSD; add tests
On FreeBSD and Darwin, changing a process's supplementary groups with
setgroups(2) will also change the egid of the process, setting it to the
first entry in the provided list. This is distinct from the behaviour on
other platforms (and possibly a violation of the POSIX standard).

Because of this, on FreeBSD with no TTY, our incubator code would
previously not change the process's gid, because it would read the
newly-changed egid, compare it against the expected egid, and since they
matched, not change the gid. Because we didn't use the 'login' program
on FreeBSD without a TTY, this would propagate to a child process.

This could be observed by running "id -p" in two contexts. The expected
output, and the output returned when running from a SSH shell, is:

    andrew@freebsd:~ $ id -p
    uid         andrew
    groups      andrew

However, when run via "ssh andrew@freebsd id -p", the output would be:

    $ ssh andrew@freebsd id -p
    login       root
    uid         andrew
    rgid        wheel
    groups      andrew

(this could also be observed via "id -g -r" to print just the gid)

We fix this by pulling the details of privilege dropping out into their
own function and prepending the expected gid to the start of the list on
Darwin and FreeBSD.

Finally, we add some tests that run a child process, drop privileges,
and assert that the final UID/GID/additional groups are what we expect.

More information can be found in the following article:
    https://www.usenix.org/system/files/login/articles/325-tsafrir.pdf

Updates #7616
Alternative to #7609

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0e6513c31b121108b50fe561c89e5816d84a45b9
2023-03-20 16:09:18 -04:00
Mihai Parparita
e1fb687104 cmd/tailscale/cli: fix inconsistency between serve text and example command
Use the same local port number in both, and be more precise about what
is being forwarded

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-20 11:52:46 -07:00
Anton Tolchanov
654b5a0616 derp: add optional debug logging for prober clients
This allows tracking packet flow via logs for prober clients. Note that
the new sclient.debug() function is called on every received packet, but
will do nothing for most clients.

I have adjusted sclient logging to print public keys in short format
rather than full. This takes effect even for existing non-debug logging
(mostly client disconnect messages).

Example logs for a packet being sent from client [SbsJn] (connected to
derper [dM2E3]) to client [10WOo] (connected to derper [AVxvv]):

```
derper [dM2E3]:
derp client 10.0.0.1:35470[SbsJn]: register single client mesh("10.0.1.1"): 4 peers
derp client 10.0.0.1:35470[SbsJn]: read frame type 4 len 40 err <nil>
derp client 10.0.0.1:35470[SbsJn]: SendPacket for [10WOo], forwarding via <derphttp_client.Client [AVxvv] url=https://10.0.1.1/derp>: <nil>
derp client 10.0.0.1:35470[SbsJn]: read frame type 0 len 0 err EOF
derp client 10.0.0.1:35470[SbsJn]: read EOF
derp client 10.0.0.1:35470[SbsJn]: sender failed: context canceled
derp client 10.0.0.1:35470[SbsJn]: removing connection

derper [AVxvv]:
derp client 10.0.1.1:50650[10WOo]: register single client
derp client 10.0.1.1:50650[10WOo]: received forwarded packet from [SbsJn] via [dM2E3]
derp client 10.0.1.1:50650[10WOo]: sendPkt attempt 0 enqueued
derp client 10.0.1.1:50650[10WOo]: sendPacket from [SbsJn]: <nil>
derp client 10.0.1.1:50650[10WOo]: read frame type 0 len 0 err EOF
derp client 10.0.1.1:50650[10WOo]: read EOF
derp client 10.0.1.1:50650[10WOo]: sender failed: context canceled
derp client 10.0.1.1:50650[10WOo]: removing connection
```

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-20 17:41:37 +00:00
Anton Tolchanov
50d211d1a4 cmd/derpprobe: allow running all probes at the same time
This allows disabling spread mode, which is helpful if you are manually
running derpprobe in `--once` mode against a small number of DERP
machines.

Updates https://github.com/tailscale/corp/issues/9916

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-20 17:41:37 +00:00
Anton Tolchanov
e59dc29a55 prober: log client pubkeys on derp mesh probe failures
Updates https://github.com/tailscale/corp/issues/9916

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-03-20 17:41:37 +00:00
dependabot[bot]
60a028a4f6 .github: Bump peter-evans/create-pull-request from 4.1.4 to 4.2.4
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.4 to 4.2.4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](ad43dccb4d...38e0b6e68b)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 15:27:06 +00:00
dependabot[bot]
927e2e3e7c .github: Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 15:22:20 +00:00
Nahum Shalman
82e067e0ff build_dist.sh: make cross-compilation friendly
Signed-off-by: Nahum Shalman <nahamu@gmail.com>
2023-03-16 22:01:05 -07:00
Maisem Ali
95494a155e .github: use unique names for jobs
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-16 10:05:27 -07:00
James 'zofrex' Sanderson
9534783758 tailscale/cmd: Warn for up --force-reauth over SSH without accepting the risk (#7575)
Fixes #6377

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2023-03-16 15:47:24 +00:00
Maisem Ali
f34590d9ed tsnet: add test for Funnel connections
For the logic added in b797f77.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-15 19:49:22 -07:00
Maisem Ali
c6d96a2b61 tsnet: do not start logtail in tests
It was trying to upload logs in tests.

skip-issuebot

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-15 18:31:41 -07:00
David Anderson
0498d5ea86 tool/gocross: delete bootstrap tarball downloads after use
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-15 11:55:02 -07:00
David Anderson
1f95bfedf7 tool/gocross: adjust Xcode flags to match new Xcode env
Xcode changed how/what data it exports to build steps at some point
recently, so our old way of figuring out the minimum support version
for clang stopped working.

Updates tailscale/corp#4095

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-15 10:58:31 -07:00
Kurnia D Win
9526858b1e control/controlclient: fix accidental backoff reset
Signed-off-by: Kurnia D Win <kurnia.d.win@gmail.com>
2023-03-15 10:25:48 -07:00
David Anderson
df3996cae3 tool/gocross: bootstrap correctly on an older toolchain
Sometimes, our cached toolchain ends up being an older version of
Go, older than our go.mod allows. In that scenario, gocross-wrapper.sh
would find a usable toolchain, but then fail to compile gocross.

This change makes the wrapper script check that the cached toolchain's
minor version is good enough to build tailscale.com, and re-bootstraps
in shell if not.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-15 09:40:30 -07:00
Mihai Parparita
97b6d3e917 sockstats: remove per-interface stats from Get
They're not needed for the sockstats logger, and they're somewhat
expensive to return (since they involve the creation of a map per
label). We now have a separate GetInterfaces() method that returns
them instead (which we can still use in the PeerAPI debug endpoint).

If changing sockstatlog to sample at 10,000 Hz (instead of the default
of 10Hz), the CPU usage would go up to 59% on a iPhone XS. Removing the
per-interface stats drops it to 20% (a no-op implementation of Get that
returns a fixed value is 16%).

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-14 15:38:41 -07:00
David Anderson
9ebab961c9 version/mkversion: don't break on tagged go.mod entries
I thought our versioning scheme would make go.mod include a commit hash
even on stable builds. I was wrong. Fortunately, the rest of this code
wants anything that 'git rev-parse' understands (to convert it into a full
git hash), and tags qualify.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-14 14:25:18 -07:00
Denton Gentry
6d3490f399 VERSION.txt: this is 1.39
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-14 13:50:57 -07:00
License Updater
51b0169b10 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-03-14 12:44:27 -07:00
Maisem Ali
b4d3e2928b tsnet: avoid deadlock on close
tsnet.Server.Close was calling listener.Close with the server mutex
held, but the listener close method tries to grab that mutex, resulting
in a deadlock.

Co-authored-by: David Crawshaw <crawshaw@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 20:50:52 -07:00
shayne
2b892ad6e7 cmd/tailscale/cli: [serve] rework commands based on feedback (#6521)
```
$ tailscale serve https:<port> <mount-point> <source> [off]
$ tailscale serve tcp:<port> tcp://localhost:<local-port> [off]
$ tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
$ tailscale serve status [--json]

$ tailscale funnel <serve-port> {on|off}
$ tailscale funnel status [--json]
```

Fixes: #6674

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-13 21:43:28 -04:00
Will Norris
6ef2105a8e log/sockstatlog: only start once; don't copy ticker
Signed-off-by: Will Norris <will@tailscale.com>
2023-03-13 17:02:42 -07:00
Maisem Ali
8c4adde083 log/sockstatlog: also shutdown the poll goroutine
Co-authored-by: Will Norris <will@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 16:39:27 -07:00
Maisem Ali
c87782ba9d cmd/k8s-operator: drop trailing dot in tagged node name
Also update tailcfg docs.

Updates #5055

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 15:39:42 -07:00
Will Norris
09e0ccf4c2 ipn: add c2n endpoint for sockstats logs
Signed-off-by: Will Norris <will@tailscale.com>
2023-03-13 15:25:54 -07:00
Will Norris
a1d9f65354 ipn,log: add logger for sockstat deltas
Signed-off-by: Will Norris <will@tailscale.com>
Co-authored-by: Melanie Warrick <warrick@tailscale.com>
2023-03-13 15:07:28 -07:00
Maisem Ali
5e8a80b845 all: replace /kb/ links with /s/ equivalents
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 14:21:15 -07:00
Maisem Ali
558735bc63 cmd/k8s-operator: require HTTPS to be enabled for AuthProxy
Updates #5055

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 12:32:55 -07:00
Maisem Ali
489e27f085 cmd/k8s-operator: make auth proxy pass tags as Impersonate-Group
We were not handling tags at all, pass them through as Impersonate-Group headers.
And use the FQDN for tagged nodes as Impersonate-User.

Updates #5055

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 12:32:12 -07:00
Maisem Ali
56526ff57f tailcfg: bump capver for 1.38
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 11:52:15 -07:00
Maisem Ali
09aed46d44 cmd/tailscale/cli: update docs and unhide configure
Also call out Alpha.

Updates #7220

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 11:36:08 -07:00
Maisem Ali
223713d4a1 tailcfg,all: add and use Node.IsTagged()
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-13 08:44:25 -07:00
Andrew Dunham
83fa17d26c various: pass logger.Logf through to more places
Updates #7537

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id89acab70ea678c8c7ff0f44792d54c7223337c6
2023-03-12 12:38:38 -04:00
Maisem Ali
958c89470b tsnet: add CertDomains helper (#7533)
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-11 16:12:57 -05:00
shayne
e109cf9fdd tsnet/tsnet: clear ipn.ServeConfig on Up for tsnet apps (#7534)
We persist the ServeConfig, even for tsnet apps. It's quite possible for
the ServeConfig to be out of step with the code. Example: If you run
`ListenFunnel` then later turn it off, the ServeConfig will still show
it enabled, the admin console will show it enabled, but the packet
handler will reject the packets.

Workaround by clearing the ServeConfig in `tsnet.Up`

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-11 16:07:22 -05:00
Maisem Ali
3ff44b2307 ipn: add Funnel port check from nodeAttr
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-11 11:20:52 -08:00
Maisem Ali
ccdd534e81 tsnet: add ListenFunnel
This lets a tsnet binary share a server out over Tailscale Funnel.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-03-11 10:34:52 -08:00
Denton Gentry
047b324933 scripts/installer: add PureOS and Amazon Linux Next
Fixes https://github.com/tailscale/tailscale/issues/7410

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-10 15:22:27 -08:00
Andrew Dunham
f0d6228c52 ipn/localapi: flesh out the 'debug derp' checks
Updates #6526

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic18d9ff288b9c7b8d5ab1bd77dd59693cd776cc4
2023-03-10 13:47:34 -05:00
License Updater
920de86cee licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-03-09 21:46:04 -08:00
Mihai Parparita
b64d78d58f sockstats: refactor validation to be opt-in
Followup to #7499 to make validation a separate function (
GetWithValidation vs. Get). This way callers that don't need it don't
pay the cost of a syscall per active TCP socket.

Also clears the conn on close, so that we don't double-count the stats.

Also more consistently uses Go doc comments for the exported API of the
sockstats package.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-09 14:31:20 -08:00
Mihai Parparita
ea81bffdeb sockstats: export as client metrics
Though not fine-grained enough to be useful for detailed analysis, we
might as well export that we gather as client metrics too, since we have
an upload/analysis pipeline for them.

clientmetric.Metric.Add is an atomic add, so it's pretty cheap to also
do per-packet.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-09 14:22:11 -08:00
Maisem Ali
1e72de6b72 ipn/ipnlocal: remove WIP restriction for Tailscale SSH on macOS
It kinda works fine now on macOS with the recent fixes in 0582829 and
 5787989d.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-09 13:52:37 -08:00
Tom DNetto
92fc243755 cmd/tailscale: annotate tailnet-lock keys which wrap pre-auth keys
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-09 11:21:39 -10:00
Tom DNetto
3471fbf8dc cmd/tailscale: surface node-key for locked out tailnet-lock peers
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-09 11:06:23 -10:00
Maisem Ali
b797f773c7 ipn/ipnlocal: add support for funnel in tsnet
Previously the part that handled Funnel connections was not
aware of any listeners that tsnet.Servers might have had open
so it would check against the ServeConfig and fail.

Adding a ServeConfig for a TCP proxy was also not suitable in this
scenario as that would mean creating two different listeners and have
one forward to the other, which really meant that you could not have
funnel and tailnet-only listeners on the same port.

This also introduces the ipn.FunnelConn as a way for users to identify
whether the call is coming over funnel or not. Currently it only holds
the underlying conn and the target as presented in the "Tailscale-Ingress-Target"
header.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-09 12:53:00 -08:00
Joe Tsai
dad78f31f3 syncs: add WaitGroup wrapper (#7481)
The addition of WaitGroup.Go in the standard library has been
repeatedly proposed and rejected.
See golang/go#18022, golang/go#23538, and golang/go#39863

In summary, the argument for WaitGroup.Go is that it avoids bugs like:

	go func() {
		wg.Add(1)
		defer wg.Done()
		...
	}()

where the increment happens after execution (not before)
and also (to a lesser degree) because:

	wg.Go(func() {
		...
	})

is shorter and more readble.

The argument against WaitGroup.Go is that the provided function
takes no arguments and so inputs and outputs must closed over
by the provided function. The most common race bug for goroutines
is that the caller forgot to capture the loop iteration variable,
so this pattern may make it easier to be accidentally racy.
However, that is changing with golang/go#57969.

In my experience the probability of race bugs due to the former
still outwighs the latter, but I have no concrete evidence to prove it.

The existence of errgroup.Group.Go and frequent utility of the method
at least proves that this is a workable pattern and
the possibility of accidental races do not appear to
manifest as frequently as feared.

A reason *not* to use errgroup.Group everywhere is that there are many
situations where it doesn't make sense for the goroutine to return an error
since the error is handled in a different mechanism
(e.g., logged and ignored, formatted and printed to the frontend, etc.).
While you can use errgroup.Group by always returning nil,
the fact that you *can* return nil makes it easy to accidentally return
an error when nothing is checking the return of group.Wait.
This is not a hypothetical problem, but something that has bitten us
in usages that was only using errgroup.Group without intending to use
the error reporting part of it.

Thus, add a (yet another) variant of WaitGroup here that
is identical to sync.WaitGroup, but with an extra method.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-09 12:04:38 -08:00
Maisem Ali
be027a9899 control/controlclient: improve handling of concurrent lite map requests
This reverts commit 6eca47b16c and fixes forward.

Previously the first ever streaming MapRequest that a client sent would also
set ReadOnly to true as it didn't have any endpoints and expected/relied on the
map poll to restart as soon as it got endpoints. However with 48f6c1eba4,
we would no longer restart MapRequests as frequently as we used to, so control
would only ever get the first streaming MapRequest which had ReadOnly=true.

Control would treat this as an uninteresting request and would not send it
any further netmaps, while the client would happily stay in the map poll forever
while litemap updates happened in parallel.

This makes it so that we never set `ReadOnly=true` when we are doing a streaming
MapRequest. This is no longer necessary either as most endpoint discovery happens
over disco anyway.

Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-09 11:36:44 -08:00
Joe Tsai
87b4bbb94f tstime/rate: add Value (#7491)
Add Value, which measures the rate at which an event occurs,
exponentially weighted towards recent activity.
It is guaranteed to occupy O(1) memory, operate in O(1) runtime,
and is safe for concurrent use.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-09 11:13:09 -08:00
Mihai Parparita
4c2f67a1d0 net/sockstat: fix per-interface statistics not always being available
withSockStats may be called before setLinkMonitor, in which case we
don't have a populated knownInterfaces map. Since we pre-populate the
per-interface counters at creation time, we would end up with an
empty map. To mitigate this, we do an on-demand request for the list of
interfaces.

This would most often happen with the logtail instrumentation, since we
initialize it very early on.

Updates tailscale/corp#9230

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-09 10:38:45 -08:00
Maisem Ali
e69682678f ssh/tailssh: use context.WithCancelCause
It was using a custom implmentation of the context.WithCancelCause,
replace usage with stdlib.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-09 10:22:55 -08:00
Mihai Parparita
a2be1aabfa logtail: remove unncessary response read
Effectively reverts #249, since the server side was fixed (with #251?)
to send a 200 OK/content-length 0 response.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-08 15:39:04 -08:00
Tom DNetto
ce99474317 all: implement preauth-key support with tailnet lock
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-08 11:56:46 -10:00
Mihai Parparita
f4f8ed98d9 sockstats: add validation for TCP socket stats
We can use the TCP_CONNECTION_INFO getsockopt() on Darwin to get
OS-collected tx/rx bytes for TCP sockets. Since this API is not available
for UDP sockets (or on Linux/Android), we can't rely on it for actual
stats gathering.

However, we can use it to validate the stats that we collect ourselves
using read/write hooks, so that we can be more confident in them. We
do need additional hooks from the Go standard library (added in
tailscale/go#59) to be able to collect them.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-08 13:39:30 -08:00
Tom DNetto
6eca47b16c Revert "control/controlclient: improve handling of concurrent lite map requests"
This reverts commit 48f6c1eba4.

It unfortunately breaks mapresponse wakeups.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-08 09:47:44 -10:00
Andrew Dunham
48f6c1eba4 control/controlclient: improve handling of concurrent lite map requests
Prior to this change, if we were in the middle of a lite map update we'd
tear down the entire map session and restart it. With this change, we'll
cancel an in-flight lite map request up to 10 times and restart before
we tear down the streaming map request. We tear down everything after 10
retries to ensure that a steady stream of calls to sendNewMapRequest
doesn't fail to make progress by repeatedly canceling and restarting.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Change-Id: I9392bf8cf674e7a58ccd1e476039300a359ef3b1
2023-03-07 19:29:55 -05:00
Maisem Ali
b0cb39cda1 tsnet: only intercept TCP flows that have listeners
Previously, it would accept all TCP connections and then close the ones
it did not care about. Make it only ever accept the connections that it
cares about.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-07 15:48:49 -08:00
M. J. Fromberger
c09578d060 .github: update tibdex/github-app-token to release v1.8.0 (#7495)
The main motivation for this change is to stop using the deprecated
set-output function which triggers deprecation warnings in the action.

Change-Id: I80496c44ea1166b9c40d5cd9e450129778ad4aaf
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2023-03-07 14:30:19 -08:00
M. J. Fromberger
a75360ccd6 util: add truncate package (#7490)
This package handles cases where we need to truncate human-readable text to fit
a length constraint without leaving "ragged" multi-byte rune fragments at the
end of the truncated value.

Change-Id: Id972135d1880485f41b1fedfb65c2b8cc012d416
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2023-03-07 11:51:36 -08:00
David Anderson
5b68dcc8c1 go.mod.sri: update for toolchain change.
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-07 11:40:29 -08:00
Kyle Carberry
3862a1e1d5 derp/derphttp: cleanup WebSocket connection on close
This was causing a leak in our CI!

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2023-03-07 11:36:34 -08:00
Andrew Dunham
be107f92d3 wgengine/magicsock: track per-endpoint changes in ringbuffer
This change adds a ringbuffer to each magicsock endpoint that keeps a
fixed set of "changes"–debug information about what updates have been
made to that endpoint.

Additionally, this adds a LocalAPI endpoint and associated
"debug peer-status" CLI subcommand to fetch the set of changes for a given
IP or hostname.

Updates tailscale/corp#9364

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I34f726a71bddd0dfa36ec05ebafffb24f6e0516a
2023-03-07 13:53:03 -05:00
David Crawshaw
9245d813c6 tsnet: explicit message for panic seen in CI
Updates #7488

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-03-07 10:22:50 -08:00
shayne
f7a7957a11 sniproxy: add promote-https (#7487)
Adds support for an HTTP server that promotes all requests to HTTPS.
The flag is `-promote-https` and defaults to true.

Updates #1748
2023-03-07 11:46:02 -05:00
Brad Fitzpatrick
49e2d3a7bd words: add word we forgot
I explained this tails/scales list to my 5yo and he looked at me like
it was the most obvious idea ever. Of course we'd make such lists at
work!  What else do grown-ups do all day? And then he wouldn't stop
talking about coelacanths and I had no clue what he was saying or how
to spell it until I asked my phone and the phone apparently understood
me and I realized it was a fish and he was helping me? I think?

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-03-06 20:53:46 -08:00
Denton Gentry
b46c5ae82a cmd/sniproxy: draw the rest of the DNS owl.
Add a DNS server which always responds as its own IP addresses.

Additionally add a tsnet TailscaleIPs() function to return the
IP addresses, both IPv4 and IPv6.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-06 19:29:01 -08:00
Joe Tsai
7e6c5a2db4 tstime: rely on stdlib parse functionality (#7482)
The time.Parse function has been optimized to the point
where it is faster than our custom implementation.
See upstream changes in:

* https://go.dev/cl/429862
* https://go.dev/cl/425197
* https://go.dev/cl/425116

Performance:

	BenchmarkGoParse3339/Z     38.75 ns/op    0 B/op    0 allocs/op
	BenchmarkGoParse3339/TZ    54.02 ns/op    0 B/op    0 allocs/op
	BenchmarkParse3339/Z       40.17 ns/op    0 B/op    0 allocs/op
	BenchmarkParse3339/TZ      87.06 ns/op    0 B/op    0 allocs/op

We can see that the stdlib implementation is now faster.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-06 18:05:51 -08:00
Joe Tsai
9112e78925 tstime: add Sleep (#7480)
Sleep is an interruptible sleep variation.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-06 17:40:38 -08:00
License Updater
3b18e65c6a licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-03-06 16:03:27 -08:00
Mihai Parparita
6ac6ddbb47 sockstats: switch label to enum
Makes it cheaper/simpler to persist values, and encourages reuse of
labels as opposed to generating an arbitrary number.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-06 15:54:35 -08:00
Aaron Klotz
9687f3700d net/dns: deal with Windows wsl.exe hangs
Despite the fact that WSL configuration is still disabled by default, we
continue to log the machine's list of WSL distros as a diagnostic measure.

Unfortunately I have seen the "wsl.exe -l" command hang indefinitely. This patch
adds a (more than reasonable) 10s timeout to ensure that tailscaled does not get
stuck while executing this operation.

I also modified the Windows implementation of NewOSConfigurator to do the
logging asynchronously, since that information is not required in order to
continue starting up.

Fixes #7476

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-03-06 16:08:13 -07:00
Tom DNetto
2263d9c44b cmd/tsconnect: pop CTA to make everything work with tailnet lock
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-06 10:07:20 -10:00
David Crawshaw
387b68fe11 tsnet: generalize loopback listener to include SOCKS5
Some languages do not give you any useful access to the sockets
underlying their networking packages. E.g. java.net.http.HttpClient
provides no official access to its dialing logic.

...but everyone supports proxies. So add a SOCKS5 proxy on the listener
we are already running.

(The function being revamped is very new,
I only added it in the last week and it wasn't part of any release,
so I believe it is fine to redo its function signature.)

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-03-06 09:00:09 -08:00
Andrew Dunham
df2561f6a2 ipn/ipnlocal: stop netmap expiry timer when resetting control client
This prevents a panic where we synthesize a new netmap in
setClientStatus after we've shut down and nil'd out the controlclient,
since that function expects to be called while connected to control.

Fixes #7392

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib631eb90f34f6afa008d69bbb386f70da145e102
2023-03-06 10:59:47 -05:00
David Crawshaw
96a555fc5a net/socks5: add password auth support
Conforms to RFC 1929.

To support Java HTTP clients via libtailscale, who offer no other
reliable hooks into their sockets.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-03-05 14:08:34 -08:00
Brad Fitzpatrick
0f4359116e tsnet: add UDP support to Server.Listen
No ListenPacket support yet, but Listen with a udp network type fit
easier into netstack's model to start.

Then added an example of using it to cmd/sniproxy with a little udp
:53 handler.

No tests in tsnet yet because we don't have support for dialing over
UDP in tsnet yet. When that's done, a new test can test both sides.

Updates #5871
Updates #1748

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-03-05 12:40:13 -08:00
Brad Fitzpatrick
9ff51ca17f wgengine/netstack: add support for custom UDP flow handlers
To be used by tsnet and sniproxy later.

Updates #5871
Updates #1748

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-03-05 09:05:43 -08:00
Andrew Dunham
045f995203 ipn/localapi: close portmapper after debug
This ensures that any mappings that are created are correctly cleaned
up, instead of waiting for them to expire in the router.

Updates #7377

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I436248ee7740eded6d8adae5df525e785a8f7ccb
2023-03-05 10:20:04 -05:00
Andrew Dunham
f6cd24499b net/portmapper: relax source port check for UPnP responses
Per a packet capture provided, some gateways will reply to a UPnP
discovery packet with a UDP packet with a source port that does not come
from the UPnP port. Accept these packets with a log message.

Updates #7377

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I5d4d5b2a0275009ed60f15c20b484fe2025d094b
2023-03-04 22:10:14 -05:00
Andrew Dunham
51eb0b2cb7 net/portmapper: send UPnP protocol in upper-case
We were previously sending a lower-case "udp" protocol, whereas other
implementations like miniupnp send an upper-case "UDP" protocol. For
compatibility, use an upper-case protocol instead.

Updates #7377

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4aed204f94e4d51b7a256d29917af1536cb1b70f
2023-03-04 16:18:26 -05:00
Andrew Dunham
d379a25ae4 net/portmapper: don't pick external ports below 1024
Some devices don't let you UPnP portmap a port below 1024, so let's just
avoid that range of ports entirely.

Updates #7377

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib7603b1c9a019162cdc4fa21744a2cae48bb1d86
2023-03-04 16:04:23 -05:00
Andrew Dunham
69f9c17555 ipn/localapi: fix panic after handler returns
Change-Id: I612f9ebf78d962e094bff908670b3ffd89f756e5
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2023-03-04 16:03:36 -05:00
Maisem Ali
1a30b2d73f all: use tstest.Replace more
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-04 12:24:55 -08:00
License Updater
57a44846ae licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-03-04 07:13:20 -08:00
Andrew Dunham
a9c17dbf93 ipn/ipnlocal: reject unmasked routes
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic804efd24f5f536de1f2c910de3a24372d48d54d
2023-03-03 22:39:01 -05:00
Andrew Dunham
2d3ae485e3 net/interfaces: add better test for LikelyHomeRouterIP
Return a mock set of interfaces and a mock gateway during this test and
verify that LikelyHomeRouterIP returns the outcome we expect. Also
verify that we return an error if there are no IPv4 addresses available.

Follow-up to #7447

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8f06989e7f1f0bebd108861cbff17b820ed2e6e4
2023-03-03 20:52:57 -05:00
Maisem Ali
b9ebf7cf14 tstest: add method to Replace values for tests
We have many function pointers that we replace for the duration of test and
restore it on test completion, add method to do that.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-03 17:02:33 -08:00
Andrew Dunham
12100320d2 net/interfaces: always return an IPv4 LikelyHomeRouterIP
We weren't filtering out IPv6 addresses from this function, so we could
be returning an IPv4 gateway IP and an IPv6 self IP. Per the function
comments, only return IPv4 addresses for the self IP.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If19a4aadc343fbd4383fc5290befa0eff006799e
2023-03-03 18:36:07 -05:00
Andrew Dunham
73fa7dd7af util/slicesx: add package for generic slice functions, use
Now that we're using rand.Shuffle in a few locations, create a generic
shuffle function and use it instead. While we're at it, move the
interleaveSlices function to the same package for use.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0b00920e5b3eea846b6cedc30bd34d978a049fd3
2023-03-03 16:25:48 -05:00
Tom DNetto
88c7d19d54 tka: compact TKA storage on startup
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-03 10:09:26 -10:00
Tom DNetto
e2d652ec4d ipn,cmd/tailscale: implement resigning nodes on tka key removal
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-03 10:09:05 -10:00
Andrew Dunham
3f8e8b04fd cmd/tailscale, cmd/tailscaled: move portmapper debugging into tailscale CLI
The debug flag on tailscaled isn't available in the macOS App Store
build, since we don't have a tailscaled binary; move it to the
'tailscale debug' CLI that is available on all platforms instead,
accessed over LocalAPI.

Updates #7377

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I47bffe4461e036fab577c2e51e173f4003592ff7
2023-03-03 14:21:38 -05:00
Mihai Parparita
3e71e0ef68 net/sockstats: remove explicit dependency on wgengine/monitor
Followup to #7177 to avoid adding extra dependencies to the CLI. We
instead declare an interface for the link monitor.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-03 08:37:14 -08:00
James Tucker
7b73c9628d version/distro,wgengine/router: raise WSL eth0 MTU when too low
WSL has started to set the eth0 default route interface default to 1280
MTU, which is too low to carry 1280 byte packets from tailscale0 once
wrapped in WireGuard. The change down to 1280 is very likely smaller
than necessary for almost all users. We can not easily determine the
ideal MTU, but if all the preconditions match, we raise the MTU to 1360,
which is just enough for Tailscale traffic to work.

Updates #4833
Updates #7346

Signed-off-by: James Tucker <james@tailscale.com>
2023-03-02 21:33:02 -08:00
shayne
d92ef4c215 cmd/derper: randomize IPs on refreshBootstrapDNS (#7440)
This is to address a possible DNS failure on startup. Before this
change IPv6 addresses would be listed first, and the client dialer would
fail for hosts without IPv6 connectivity.
2023-03-02 23:36:12 -05:00
Andrew Dunham
27575cd52d net/dnsfallback: shuffle returned IPs
This ensures that we're trying multiple returned IPs, since the DERP
servers return the same response to all queries. This should increase
the chances that we eventually reach a working IP.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie8d4fb93df96da910fae49ae71bf3e402b9fdecc
2023-03-02 22:55:58 -05:00
Julia at Tailscale
ef6f66bb9a api.md: refresh of API docs
Update API documentation to include explanation of resources, a cleaner and more consistent structure, updated terminology, and fixes to a few errors and omissions.

Signed-off-by: Julia Stein <julia@tailscale.com>
Signed-off-by: Will Norris <will@tailscale.com>
Co-authored-by: Will Norris <will@tailscale.com>
2023-03-02 17:20:45 -08:00
Brad Fitzpatrick
1410682fb6 cmd/sniproxy: add start of a tsnet-based SNI proxy
$ curl https://canhazip.com/
    170.173.0.21
    $ curl --resolve canhazip.com:443:100.85.165.81 https://canhazip.com/
    34.223.127.151

Updates #1748

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-03-02 14:39:10 -08:00
Joe Tsai
283a84724f types/logid: simplify implementation (#7415)
Share the same underlying implementation for both PrivateID and PublicID.
For the shared methods, declare them in the same order.
Only keep documentation on methods without obvious meaning.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-02 13:18:04 -08:00
Maisem Ali
e1530cdfcc cmd/containerboot,kube: consolidate the two kube clients
We had two implemenetations of the kube client, merge them.

containerboot was also using a raw http.Transport, this also has
the side effect of making it use a http.Client

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-03-02 11:36:06 -08:00
Flakes Updater
5eb8a2a86a go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-03-02 11:02:05 -08:00
Andrew Dunham
d8286d0dc2 go.mod: bump golang.org/x/image to latest version
This resolves a dependabot alert, though the alert does not affect us:
    https://github.com/tailscale/tailscale/security/dependabot/6

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I087de6f22fb4821d0035fc16b603f9692581b9bd
2023-03-02 13:56:37 -05:00
Denton Gentry
51288221ce cmd/tailscale: use request Schema+Host for QNAP authLogin.cgi
QNAP allows users to set the port number for the management WebUI,
which includes authLogin.cgi. If they do, then connecting to
localhost:8080 fails.

https://github.com/tailscale/tailscale-qpkg/issues/74#issuecomment-1407486911

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-01 18:00:06 -08:00
tailscale-license-updater[bot]
06302e30ae licenses: update win/apple licenses (#7423)
Signed-off-by: License Updater <noreply@tailscale.com>
Co-authored-by: License Updater <noreply@tailscale.com>
2023-03-01 17:51:17 -08:00
David Anderson
311352d195 release/dist/cli: add --verbose to print subcommand output
By default, cmd/dist only prints the output of failed commands.
With this, you can turn all the noisy output back on.

Updates tailscale/corp#9045

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-01 17:16:31 -08:00
David Anderson
0df11253ec release/dist: add a helper to run commands
The helper suppresses output if the command runs successfully. If the
command fails, it dumps the buffered output to stdout before returning
the error. This means the happy path isn't swamped by debug noise or
xcode being intensely verbose about what kind of day it's having,
but you still get debug output when something goes wrong.

Updates tailscale/corp#9045

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-03-01 17:16:31 -08:00
Aaron Klotz
f18beaa1e4 cmd/mkmanifest, cmd/tailscale, cmd/tailscaled: remove Windows arm32 resources from OSS
Given recent changes in corp, I originally thought we could remove all of the
syso files, but then I realized that we still need them so that binaries built
purely from OSS (without going through corp) will still receive a manifest.

We can remove the arm32 one though, since we don't support 32-bit ARM on Windows.

Updates https://github.com/tailscale/corp/issues/9576

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-03-01 15:45:12 -07:00
Sonia Appasamy
7985f5243a cmd/k8s-operator: update device authorization copy
"Device Authorization" was recently renamed to "Device Approval"
on the control side. This change updates the k8s operator to match.

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-03-01 16:39:15 -05:00
Tom DNetto
ff168a806e tka: implement compaction logic
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-03-01 10:50:07 -10:00
Sonia Appasamy
bb7033174c cmd/tsconnect: update device authorization copy
"Device Authorization" was recently renamed to "Device Approval"
on the control side. This change updates tsconnect to match.

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-03-01 15:23:29 -05:00
Joe Tsai
7e4788e383 logtail: delete ID types and functions (#7412)
These have been moved to the types/logid package.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-03-01 12:18:23 -08:00
Mihai Parparita
9cb332f0e2 sockstats: instrument networking code paths
Uses the hooks added by tailscale/go#45 to instrument the reads and
writes on the major code paths that do network I/O in the client. The
convention is to use "<package>.<type>:<label>" as the annotation for
the responsible code path.

Enabled on iOS, macOS and Android only, since mobile platforms are the
ones we're most interested in, and we are less sensitive to any
throughput degradation due to the per-I/O callback overhead (macOS is
also enabled for ease of testing during development).

For now just exposed as counters on a /v0/sockstats PeerAPI endpoint.

We also keep track of the current interface so that we can break out
the stats by interface.

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-03-01 12:09:31 -08:00
Sonia Appasamy
0c1510739c cmd/tailscale/cli: update device authorization copy
"Device Authorization" was recently renamed to "Device Approval"
on the control side. This change updates the linux cli to match.

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-03-01 14:02:28 -05:00
Joe Tsai
06134e9521 types/logid: remove MustParsePublicID (#7405)
Ever since the introduction of the "must" package,
most MustXXX functions are no longer necessary.
Remove this as it is no longer depended upon
from within this repository and by the internal private repository.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-02-28 19:00:11 -08:00
Joe Tsai
0d19f5d421 all: replace logtail.{Public,Private}ID with logid.{Public,Private}ID (#7404)
The log ID types were moved to a separate package so that
code that only depend on log ID types do not need to link
in the logic for the logtail client itself.
Not all code need the logtail client.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-02-28 19:00:00 -08:00
David Crawshaw
d41f6a8752 tsnet: do not error on NeedsMachineAuth for Up
It turns out even with an AuthKey that pre-approves devices on a tailnet
with machine auth turned on, we still temporarily see the
NeedsMachineAuth state. So remove that error (for now).

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-28 15:36:33 -08:00
David Crawshaw
768df4ff7a tsnet: add a LocalAPI listener on loopback, with basic auth
This is for use by LocalAPI clients written in other languages that
don't appear to be able to talk HTTP over a socket (e.g.
java.net.http.HttpClient).

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-28 13:52:32 -08:00
Vladimir Pouzanov
e3211ff88b Add support for OAuth tokens #7394 (#7393)
Signed-off-by: Vladimir Pouzanov <farcaller@gmail.com>
2023-02-27 18:05:24 -08:00
Maisem Ali
49c206fe1e tailcfg,hostinfo: add App field to identify tsnet uses
This allows us to differentiate between the various tsnet apps that
we have like `golinks` and `k8s-operator`.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-27 15:19:33 -08:00
Mihai Parparita
780c56e119 ipn/ipnlocal: add delegated interface information to /interfaces PeerAPI handler
Exposes the delegated interface data added by #7248 in the debug
endpoint. I would have found it useful when working on that PR, and
it may be handy in the future as well.

Also makes the interfaces table slightly easier to parse by adding
borders to it. To make then nicer-looking, the CSP was relaxed to allow
inline styles.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-02-27 09:39:49 -08:00
Charlotte Brandhorst-Satzkorn
e484e1c0fc words: just words, nothing but words (#7384)
nothing in relation to fish at all.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-02-26 11:29:45 -08:00
Denton Gentry
bf7573c9ee cmd/nginx-auth: build for arm64
Fixes https://github.com/tailscale/tailscale/issues/6978

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-25 17:16:31 -08:00
Denton Gentry
9ab992e7a1 syncs: re-enable TestWatchMultipleValues
We've updated to a different set of CI machines since this test
was disabled.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-25 17:03:16 -08:00
Maisem Ali
0582829e00 ssh/tailssh: try launching commands with /usr/bin/login on macOS
Updates #4939

Co-authored-by: Adam Eijdenberg <adam@continusec.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-25 15:46:34 -08:00
Charlotte Brandhorst-Satzkorn
e851d134cf words: grasping at straws... wait, do straws have tails? (#7376)
One might argue they have two, but until that hypothesis can be proven
these tails and scales will have to do!

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-02-24 21:10:43 -08:00
David Anderson
04be5ea725 release/dist/cli: default to "all" for list if no filters given
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 15:26:46 -08:00
Jordan Whited
d4122c9f0a cmd/tailscale/cli: fix TestUpdatePrefs over Tailscale SSH (#7374)
Fixes #7373

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-02-24 15:26:23 -08:00
David Anderson
b0eba129e6 .github/workflows: add a pass/fail verdict job to the test workflow
Github requires explicitly listing every single job within a workflow
that is required for status checks, instead of letting you list entire
workflows. This is ludicrous, and apparently this nonsense is the
workaround.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 23:00:22 +00:00
David Anderson
0ab6a7e7f5 .github/workflows: try to make the merge queue actually run CI
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:31:47 +00:00
David Anderson
587eb32a83 release/dist: add forgotten license headers
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:21:28 +00:00
David Anderson
cf74ee49ee release/dist/cli: factor out the CLI boilerplace from cmd/dist
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:21:28 +00:00
David Anderson
fc4b25d9fd release: open-source release build logic for unix packages
Updates tailscale/corp#9221

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 21:31:09 +00:00
David Crawshaw
44e027abca tsnet: add data transfer test
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 19:18:32 +00:00
David Crawshaw
46467e39c2 logtail: allow multiple calls to Shutdown
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 19:18:32 +00:00
David Crawshaw
daa2f1c66e tsnet: add Up method to block until ready
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 18:57:55 +00:00
David Anderson
64181e17c8 tool/gocross: support local toolchain for development
This makes gocross and its bootstrap script understand an absolute
path in go.toolchain.rev to mean "use the given toolchain directly".

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 05:55:46 +00:00
David Anderson
66621ab38e tool/gocross: embed the version explicitly with linker flags
We need to build gocross from multiple repos, but Go's innate
git hash embedding only works when you build gocross from this repo,
not when you build it from elsewhere via 'go build
tailscale.com/tool/gocross'. Instead, explicitly embed the version
found with 'git rev-parse HEAD', which will work from any git repo.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 03:11:29 +00:00
David Anderson
7444dabb68 tool/gocross: do all the bootstrap steps in a subshell
This avoids accidentally overwriting variables from the input
environment, which might non-deterministically change the behavior
of gocross.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 03:11:29 +00:00
Tom DNetto
abc874b04e tka: add public API on NodeKeySignature key information
This is needed in the coordination server.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-02-23 19:20:39 +00:00
License Updater
61a345c8e1 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-02-23 18:56:08 +00:00
Maisem Ali
06a10125fc cmd/k8s-operator: set hostinfo.Package
This allows identifying the operator.

Updates #5055

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-23 02:33:23 +00:00
David Anderson
7e65a11df5 tool/gocross: write the wrapper script directly, rather than printing
Turns out directing the printed script into the bootstrap location leads
to irritating "text file busy" problems and then having to muck about with
tempfiles and chmod and all that. Instead, have gocross write everything
with the right values.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-23 02:03:14 +00:00
David Anderson
499d82af8a tool/gocross: add command to print the wrapper shell script
So that when importing and using gocross from other repos, there's
an easy way to get at the right wrapper script that's in sync with
the gocross binary.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 20:48:37 +00:00
David Anderson
860734aed9 tool/gocross: a tool for building Tailscale binaries
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 17:55:16 +00:00
David Anderson
0b8f89c79c cmd/tsconnect: find the build dir independently of -trimpath
trimmed builds don't have absolute path information in executable
metadata, which leads the runtime.Caller approach failing
mysteriously in yarn with complaints about relative package paths.

So, instead of using embedded package metadata to find paths,
expect that we're being invoked within the tailscale repo, and
locate the tsconnect directory that way.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 00:02:47 +00:00
Tom DNetto
f9b746846f tailcfg: add RPC structs for /tka/affected-sigs
These RPCs will be used to power the future 'tailscale lock remove' default behavior
of resigning signatures for which trust is about to be removed.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-02-21 21:58:38 +00:00
Andrew Dunham
e220fa65dd util/ringbuffer: move generic ringbuffer from corp repo
Also add some basic tests for this implementation.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I307ebb6db91d0c172657befb276b38ccb638f828
2023-02-21 19:11:08 +00:00
Shayne Sweeney
cd18bb68a4 gitignore: Add personal .gopath and nix build /result
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-02-21 03:01:19 +00:00
License Updater
d38abe90be licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-02-19 05:43:53 +00:00
David Anderson
5a2fa3aa95 .github/workflows: add armv5 and armv7 cross tests
armv5 because that's what we ship to most downstreams right now,
armv7 becuase that's what we want to ship more of.

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

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-19 05:16:11 +00:00
Maisem Ali
5787989d74 ssh/tailssh: detect user shell correctly on darwin
Updates #6213

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-19 01:52:13 +00:00
Denton Gentry
6dabb34c7f scripts/installer.sh: add GalliumOS and Sangoma Linux
Fixes https://github.com/tailscale/tailscale/issues/6541
Fixes https://github.com/tailscale/tailscale/issues/6555

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-18 23:13:05 +00:00
David Anderson
093139fafd .github/workflows: fix non-collapsing CI status in PRs
CI status doesn't collapse into "everything OK" if a job gets
skipped. Instead, always run the job, but skip its only step in PRs.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 22:31:05 +00:00
Nicolas BERNARD
3db894b78c client/tailscale: add tags field to Device struct
Fixes #7302

Signed-off-by: Nicolas BERNARD <nikkau@nikkau.net>
2023-02-18 21:14:40 +00:00
David Anderson
306c8a713c .github/workflows: run CI and CodeQL in the merge queue
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:21:07 +00:00
David Anderson
149de5e6d6 build_dist.sh: use cmd/mkversion to get version data
Replaces the former shell goop, which was a shell reimplementation
of a subset of version/mkversion.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:05:39 +00:00
David Anderson
45d9784f9d version/mkversion: allow collecting version only from this repo
With this change, you can collect version info from either a git
checkout of the tailscale.com Go module (this repo), or a git
checkout of a repo that imports the tailscale.com Go module.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:05:39 +00:00
Flakes Updater
303048a7d5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-02-18 18:24:33 +00:00
Brad Fitzpatrick
e8a028cf82 go.mod: bump x/crypto
No particular reason. Just good point of our release cycle for some #cleanup.

It also makes dependabot happy about something we're not using?

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-02-18 18:04:02 +00:00
Maisem Ali
a7eab788e4 metrics: add SetInt64 to ease using LabelMap for gauge metrics
Set is provided by the underlying Map.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-18 17:43:43 +00:00
Denton Gentry
1ba0b7fd79 scripts/installer.sh: add postmarketos support.
Fixes https://github.com/tailscale/tailscale/issues/7300

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-18 16:06:47 +00:00
David Anderson
7ca54c890e version/mkversion: add exports for major/minor/patch
build_dist.sh needs the minor version by itself, for some reason.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
David Anderson
8ed27d65ef version/mkversion: add documentation, rename internal terminology
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
David Anderson
1dadbbb72a version/mkversion: open-source version generation logic
In preparation for moving more of the release building here too.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
Maisem Ali
d811c5a7f0 cmd/tailscale/cli: handle home dir correctly on macOS for kubeconfig
This ensures that we put the kubeconfig in the correct directory from within the macOS Sandbox when
paired with tailscale/corp@3035ef7

Updates #7220

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-17 01:18:52 +00:00
Maisem Ali
4a99481a11 .github/workflows: set TS_FUZZ_CURRENTLY_BROKEN to false
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-17 01:18:42 +00:00
Will Norris
8b9ee7a558 Makefile: add help text to Makefile
https://rosszurowski.com/log/2022/makefiles#self-documenting-makefiles
Signed-off-by: Will Norris <will@tailscale.com>
2023-02-16 22:39:09 +00:00
382 changed files with 25947 additions and 6442 deletions

View File

@@ -17,6 +17,8 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
merge_group:
branches: [ main ]
schedule:
- cron: '31 14 * * 5'

View File

@@ -17,7 +17,7 @@ concurrency:
cancel-in-progress: true
jobs:
tailscale:
update-licenses:
runs-on: ubuntu-latest
steps:
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version-file: go.mod
@@ -42,7 +42,7 @@ jobs:
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
- name: Get access token
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
@@ -50,11 +50,11 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply@tailscale.com>
committer: License Updater <noreply@tailscale.com>
author: License Updater <noreply+license-updater@tailscale.com>
committer: License Updater <noreply+license-updater@tailscale.com>
branch: licenses/cli
commit-message: "licenses: update tailscale{,d} licenses"
title: "licenses: update tailscale{,d} licenses"

40
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: golangci-lint
on:
# For now, only lint pull requests, not the main branches.
pull_request:
# TODO(andrew): enable for main branch after an initial waiting period.
#push:
# branches:
# - main
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: go.mod
cache: false
- name: golangci-lint
# Note: this is the 'v3' tag as of 2023-04-17
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5
with:
version: v1.52.2
# Show only new issues if it's a pull request.
only-new-issues: true

102
.github/workflows/installer.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: test installer.sh
on:
push:
branches:
- "main"
paths:
- scripts/installer.sh
pull_request:
branches:
- "*"
paths:
- scripts/installer.sh
jobs:
test:
strategy:
# Don't abort the entire matrix if one element fails.
fail-fast: false
# Don't start all of these at once, which could saturate Github workers.
max-parallel: 4
matrix:
image:
# This is a list of Docker images against which we test our installer.
# If you find that some of these no longer exist, please feel free
# to remove them from the list.
# When adding new images, please only use official ones.
- "debian:oldstable-slim"
- "debian:stable-slim"
- "debian:testing-slim"
- "debian:sid-slim"
- "ubuntu:18.04"
- "ubuntu:20.04"
- "ubuntu:22.04"
- "ubuntu:22.10"
- "ubuntu:23.04"
- "elementary/docker:stable"
- "elementary/docker:unstable"
- "parrotsec/core:lts-amd64"
- "parrotsec/core:latest"
- "kalilinux/kali-rolling"
- "kalilinux/kali-dev"
- "oraclelinux:9"
- "oraclelinux:8"
- "fedora:latest"
- "rockylinux:8.7"
- "rockylinux:9"
- "amazonlinux:latest"
- "opensuse/leap:latest"
- "opensuse/tumbleweed:latest"
- "archlinux:latest"
- "alpine:3.14"
- "alpine:latest"
- "alpine:edge"
deps:
# Run all images installing curl as a dependency.
- curl
include:
# Check a few images with wget rather than curl.
- { image: "debian:oldstable-slim", deps: "wget" }
- { image: "debian:sid-slim", deps: "wget" }
- { image: "ubuntu:23.04", deps: "wget" }
# Ubuntu 16.04 also needs apt-transport-https installed.
- { image: "ubuntu:16.04", deps: "curl apt-transport-https" }
- { image: "ubuntu:16.04", deps: "wget apt-transport-https" }
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: install dependencies (yum)
# tar and gzip are needed by the actions/checkout below.
run: yum install -y --allowerasing tar gzip ${{ matrix.deps }}
if: |
contains(matrix.image, 'centos')
|| contains(matrix.image, 'oraclelinux')
|| contains(matrix.image, 'fedora')
|| contains(matrix.image, 'amazonlinux')
- name: install dependencies (zypper)
# tar and gzip are needed by the actions/checkout below.
run: zypper --non-interactive install tar gzip
if: contains(matrix.image, 'opensuse')
- name: install dependencies (apt-get)
run: |
apt-get update
apt-get install -y ${{ matrix.deps }}
if: |
contains(matrix.image, 'debian')
|| contains(matrix.image, 'ubuntu')
|| contains(matrix.image, 'elementary')
|| contains(matrix.image, 'parrotsec')
|| contains(matrix.image, 'kalilinux')
- name: checkout
uses: actions/checkout@v3
- name: run installer
run: scripts/installer.sh
# Package installation can fail in docker because systemd is not running
# as PID 1, so ignore errors at this step. The real check is the
# `tailscale --version` command below.
continue-on-error: true
- name: check tailscale version
run: tailscale --version

View File

@@ -14,7 +14,7 @@ env:
# This variable toggles the fuzz job between two modes:
# - false: we expect fuzzing to be happy, and should report failure if it's not.
# - true: we expect fuzzing is broken, and should report failure if it start working.
TS_FUZZ_CURRENTLY_BROKEN: true
TS_FUZZ_CURRENTLY_BROKEN: false
on:
push:
@@ -24,6 +24,9 @@ on:
pull_request:
branches:
- "*"
merge_group:
branches:
- "main"
concurrency:
# For PRs, later CI runs preempt previous ones. e.g. a force push on a PR
@@ -43,18 +46,36 @@ jobs:
include:
- goarch: amd64
- goarch: amd64
variant: race
buildflags: "-race"
- goarch: "386" # thanks yaml
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
- name: build all
run: ./tool/go build ./...
run: ./tool/go build ${{matrix.buildflags}} ./...
env:
GOARCH: ${{ matrix.goarch }}
- name: build variant CLIs
run: |
export TS_USE_TOOLCHAIN=1
./build_dist.sh --extra-small ./cmd/tailscaled
./build_dist.sh --box ./cmd/tailscaled
./build_dist.sh --extra-small --box ./cmd/tailscaled
@@ -69,13 +90,11 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
if: matrix.variant != 'race'
run: ./tool/go test -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper
env:
GOARCH: ${{ matrix.goarch }}
- name: test all (race)
if: matrix.variant == 'race'
run: ./tool/go test -race -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
- name: bench all
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -test.bench=. -test.benchtime=1x -test.run=^$
env:
GOARCH: ${{ matrix.goarch }}
- name: check that no tracked files changed
@@ -97,6 +116,13 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
cache: false
- name: Restore Cache
uses: actions/cache@v3
with:
@@ -105,17 +131,20 @@ jobs:
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
# TODO(raggi): add a go version here.
key: ${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-go-2-
- name: test
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: ./tool/go test -bench . -benchtime 1x ./...
run: go test -bench . -benchtime 1x ./...
vm:
runs-on: ["self-hosted", "linux", "vm"]
@@ -144,6 +173,12 @@ jobs:
goarch: "386" # thanks yaml
- goos: linux
goarch: loong64
- goos: linux
goarch: arm
goarm: "5"
- goos: linux
goarch: arm
goarm: "7"
# macOS
- goos: darwin
goarch: amd64
@@ -164,11 +199,29 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-
- name: build all
run: ./tool/go build ./cmd/...
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "0"
- name: build tests
run: ./tool/go test -exec=true ./...
@@ -212,6 +265,23 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-go-2-
- name: build tsconnect client
run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
env:
@@ -224,6 +294,15 @@ jobs:
./tool/go run ./cmd/tsconnect --fast-compression build
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v3
- name: test tailscale_go
run: ./tool/go test -tags=tailscale_go,ts_enable_sockstats ./net/sockstats/...
fuzz:
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
# of the file), so it's more complex than usual: the 'build fuzzers' step
@@ -351,8 +430,7 @@ jobs:
GOARCH: ${{ matrix.goarch }}
notify_slack:
# Only notify slack for merged commits, not PR failures.
if: failure() && github.event_name == 'push'
if: always()
# Any of these jobs failing causes a slack notification.
needs:
- android
@@ -362,6 +440,7 @@ jobs:
- cross
- ios
- wasm
- tailscale_go
- fuzz
- depaware
- go_generate
@@ -371,7 +450,15 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: notify
uses: ruby/action-slack@v3.0.0
# Only notify slack for merged commits, not PR failures.
#
# It may be tempting to move this condition into the job's 'if' block, but
# don't: Github only collapses the test list into "everything is OK" if
# all jobs succeeded. A skipped job results in the list staying expanded.
# By having the job always run, but skipping its only step as needed, we
# let the CI output collapse nicely in PRs.
if: failure() && github.event_name == 'push'
uses: ruby/action-slack@v3.2.1
with:
payload: |
{
@@ -386,3 +473,28 @@ jobs:
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
check_mergeability:
if: always()
runs-on: ubuntu-22.04
needs:
- android
- test
- windows
- vm
- cross
- ios
- wasm
- tailscale_go
- fuzz
- depaware
- go_generate
- go_mod_tidy
- licenses
- staticcheck
steps:
- name: Decide if change is okay to merge
if: github.event_name != 'push'
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -1,30 +0,0 @@
name: "@tailscale/connect npm publish"
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- name: Build package
# Build with build_dist.sh to ensure that version information is embedded.
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
# also picks up our custom Go toolchain.
run: |
./build_dist.sh tailscale.com/cmd/tsconnect
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public

View File

@@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true
jobs:
tailscale:
update-flake:
runs-on: ubuntu-latest
steps:
@@ -27,7 +27,7 @@ jobs:
run: ./update-flake.sh
- name: Get access token
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
@@ -35,11 +35,11 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
with:
token: ${{ steps.generate-token.outputs.token }}
author: Flakes Updater <noreply@tailscale.com>
committer: Flakes Updater <noreply@tailscale.com>
author: Flakes Updater <noreply+flakes-updater@tailscale.com>
committer: Flakes Updater <noreply+flakes-updater@tailscale.com>
branch: flakes
commit-message: "go.mod.sri: update SRI hash for go.mod changes"
title: "go.mod.sri: update SRI hash for go.mod changes"

9
.gitignore vendored
View File

@@ -26,5 +26,14 @@ cmd/tailscaled/tailscaled
# Ignore personal VS Code settings
.vscode/
# Support personal project-specific GOPATH
.gopath/
# Ignore nix build result path
/result
# Ignore direnv nix-shell environment cache
.direnv/
/gocross
/dist

61
.golangci.yml Normal file
View File

@@ -0,0 +1,61 @@
linters:
# Don't enable any linters by default; just the ones that we explicitly
# enable in the list below.
disable-all: true
enable:
- bidichk
- gofmt
- goimports
- misspell
- revive
# Configuration for how we run golangci-lint
run:
timeout: 5m
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# These are forks of an upstream package and thus are exempt from stylistic
# changes that would make pulling in upstream changes harder.
- path: tempfork/.*\.go
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
- path: util/singleflight/.*\.go
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
# Per-linter settings are contained in this top-level key
linters-settings:
# Enable all rules by default; we don't use invisible unicode runes.
bidichk:
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
goimports:
misspell:
revive:
enable-all-rules: false
ignore-generated-header: true
rules:
- name: atomic
- name: context-keys-type
- name: defer
arguments: [[
# Calling 'recover' at the time a defer is registered (i.e. "defer recover()") has no effect.
"immediate-recover",
# Calling 'recover' outside of a deferred function has no effect
"recover",
# Returning values from a deferred function has no effect
"return",
]]
- name: duplicated-imports
- name: errorf
- name: string-of-int
- name: time-equal
- name: unconditional-recursion
- name: useless-break
- name: waitgroup-by-value

View File

@@ -2,16 +2,13 @@ IMAGE_REPO ?= tailscale/tailscale
SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
usage:
echo "See Makefile"
vet:
vet: ## Run go vet
./tool/go vet ./...
tidy:
tidy: ## Run go mod tidy
./tool/go mod tidy
updatedeps:
updatedeps: ## Update depaware deps
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update \
@@ -19,7 +16,7 @@ updatedeps:
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
depaware:
depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
@@ -27,42 +24,42 @@ depaware:
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
buildwindows:
buildwindows: ## Build tailscale CLI for windows/amd64
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
build386:
build386: ## Build tailscale CLI for linux/386
GOOS=linux GOARCH=386 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildlinuxarm:
buildlinuxarm: ## Build tailscale CLI for linux/arm
GOOS=linux GOARCH=arm ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildwasm:
buildwasm: ## Build tailscale CLI for js/wasm
GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
buildlinuxloong64:
buildlinuxloong64: ## Build tailscale CLI for linux/loong64
GOOS=linux GOARCH=loong64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
buildmultiarchimage: ## Build (and optionally push) multiarch docker image
./build_docker.sh
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ## Perform basic checks and compilation tests
staticcheck:
staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
spk:
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
spkall:
spkall: ## Build synology packages for all architectures and DSM versions
mkdir -p spks
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
pushspk: spk
pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
scp tailscale.spk root@${SYNO_HOST}:
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
publishdevimage:
publishdevimage: ## Build and publish tailscale image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@@ -70,10 +67,18 @@ publishdevimage:
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator:
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
@echo ""
.PHONY: help
.DEFAULT_GOAL := help

View File

@@ -1 +1 @@
1.37.0
1.41.0

1845
api.md

File diff suppressed because it is too large Load Diff

View File

@@ -8,14 +8,20 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
// WriteFile writes data to filename+some suffix, then renames it into filename.
// The perm argument is ignored on Windows. If the target filename already
// exists but is not a regular file, WriteFile returns an error.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
fi, err := os.Stat(filename)
if err == nil && !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists and is not a regular file", filename)
}
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err

View File

@@ -0,0 +1,47 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !windows
package atomicfile
import (
"net"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestDoesNotOverwriteIrregularFiles(t *testing.T) {
// Per tailscale/tailscale#7658 as one example, almost any imagined use of
// atomicfile.Write should likely not attempt to overwrite an irregular file
// such as a device node, socket, or named pipe.
const filename = "TestDoesNotOverwriteIrregularFiles"
var path string
// macOS private temp does not allow unix socket creation, but /tmp does.
if runtime.GOOS == "darwin" {
path = filepath.Join("/tmp", filename)
t.Cleanup(func() { os.Remove(path) })
} else {
path = filepath.Join(t.TempDir(), filename)
}
// The least troublesome thing to make that is not a file is a unix socket.
// Making a null device sadly requires root.
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
t.Fatal(err)
}
defer l.Close()
err = WriteFile(path, []byte("hello"), 0644)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "is not a regular file") {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -11,42 +11,25 @@
set -eu
IFS=".$IFS" read -r major minor patch <VERSION.txt
git_hash=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
git_hash="${git_hash}-dirty"
fi
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
change_count=$(git rev-list --count HEAD "^$base_hash")
short_hash=$(echo "$git_hash" | cut -c1-9)
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
change_suffix="-$change_count"
else
change_suffix=""
go="go"
if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
go="./tool/go"
fi
long_suffix="$change_suffix-t$short_hash"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"
VERSION_MINOR="$VERSION_MINOR"
VERSION_SHORT="$VERSION_SHORT"
VERSION_LONG="$VERSION_LONG"
VERSION_GIT_HASH="$VERSION_GIT_HASH"
EOF
exit 0
fi
tags=""
ldflags="-X tailscale.com/version.longStamp=${LONG} -X tailscale.com/version.shortStamp=${SHORT}"
ldflags="-X tailscale.com/version.longStamp=${VERSION_LONG} -X tailscale.com/version.shortStamp=${VERSION_SHORT}"
# build_dist.sh arguments must precede go build arguments.
while [ "$#" -gt 1 ]; do

View File

@@ -103,7 +103,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// it as a string.
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
// changes are allowing comments and trailing comments. See the following links for more info:
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
// https://tailscale.com/s/acl-format
// https://github.com/tailscale/hujson
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
// Format return errors to be descriptive.
@@ -436,7 +436,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
}()
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
tests := []ACLTest{{User: source, Allow: []string{dest}}}
postData, err := json.Marshal(tests)
if err != nil {
return nil, err

View File

@@ -44,17 +44,18 @@ type Device struct {
Name string `json:"name"`
Hostname string `json:"hostname"`
ClientVersion string `json:"clientVersion"` // Empty for external devices.
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
OS string `json:"os"`
Created string `json:"created"` // Empty for external devices.
LastSeen string `json:"lastSeen"`
KeyExpiryDisabled bool `json:"keyExpiryDisabled"`
Expires string `json:"expires"`
Authorized bool `json:"authorized"`
IsExternal bool `json:"isExternal"`
MachineKey string `json:"machineKey"` // Empty for external devices.
NodeKey string `json:"nodeKey"`
ClientVersion string `json:"clientVersion"` // Empty for external devices.
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
OS string `json:"os"`
Tags []string `json:"tags"`
Created string `json:"created"` // Empty for external devices.
LastSeen string `json:"lastSeen"`
KeyExpiryDisabled bool `json:"keyExpiryDisabled"`
Expires string `json:"expires"`
Authorized bool `json:"authorized"`
IsExternal bool `json:"isExternal"`
MachineKey string `json:"machineKey"` // Empty for external devices.
NodeKey string `json:"nodeKey"`
// BlocksIncomingConnections is configured via the device's
// Tailscale client preferences. This field is only reported

View File

@@ -63,7 +63,7 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData interface{}) ([]byte, error) {
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
data, err := json.Marshal(&postData)
if err != nil {

View File

@@ -36,6 +36,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
// defaultLocalClient is the default LocalClient when using the legacy
@@ -95,8 +96,9 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
// 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.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
// We use 127.0.0.1 and not "localhost" (issue 7851).
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
@@ -367,6 +369,34 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// DebugPortmap invokes the debug-portmap endpoint, and returns an
// io.ReadCloser that can be used to read the logs that are printed during this
// process.
func (lc *LocalClient) DebugPortmap(ctx context.Context, duration time.Duration, ty, gwSelf string) (io.ReadCloser, error) {
vals := make(url.Values)
vals.Set("duration", duration.String())
vals.Set("type", ty)
if gwSelf != "" {
vals.Set("gateway_and_self", gwSelf)
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
if err != nil {
return nil, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, nil
}
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
@@ -821,6 +851,30 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
// enable unattended bringup in the locked tailnet.
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
}
var b bytes.Buffer
type wrapRequest struct {
TSKey string
TKAKey string // key.NLPrivate.MarshalText
}
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
return "", err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
if err != nil {
return "", fmt.Errorf("error: %w", err)
}
return string(body), nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
@@ -858,6 +912,15 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
return nil
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[[]tkatype.MarshaledSignature](body)
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
@@ -1039,7 +1102,6 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
res.Body.Close()
return nil, err
}
if res.StatusCode != 200 {

View File

@@ -6,138 +6,28 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"tailscale.com/kube"
"tailscale.com/tailcfg"
"tailscale.com/util/multierr"
)
// checkSecretPermissions checks the secret access permissions of the current
// pod. It returns an error if the basic permissions tailscale needs are
// missing, and reports whether the patch permission is additionally present.
//
// Errors encountered during the access checking process are logged, but ignored
// so that the pod tries to fail alive if the permissions exist and there's just
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
// should always be able to use SSARs to assess their own permissions, but since
// we didn't use to check permissions this way we'll be cautious in case some
// old version of k8s deviates from the current behavior.
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
var errs []error
for _, verb := range []string{"get", "update"} {
ok, err := checkPermission(ctx, verb, secretName)
if err != nil {
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
} else if !ok {
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
}
}
if len(errs) > 0 {
return false, multierr.New(errs...)
}
ok, err := checkPermission(ctx, "patch", secretName)
if err != nil {
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
return false, nil
}
return ok, nil
}
// checkPermission reports whether the current pod has permission to use the
// given verb (e.g. get, update, patch) on secretName.
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
sar := map[string]any{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SelfSubjectAccessReview",
"spec": map[string]any{
"resourceAttributes": map[string]any{
"namespace": kubeNamespace,
"verb": verb,
"resource": "secrets",
"name": secretName,
},
},
}
bs, err := json.Marshal(sar)
if err != nil {
return false, err
}
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
if err != nil {
return false, err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
return false, err
}
defer resp.Body.Close()
bs, err = io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var res struct {
Status struct {
Allowed bool `json:"allowed"`
} `json:"status"`
}
if err := json.Unmarshal(bs, &res); err != nil {
return false, err
}
return res.Status.Allowed, nil
}
// findKeyInKubeSecret inspects the kube secret secretName for a data
// field called "authkey", and returns its value if present.
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
s, err := kc.GetSecret(ctx, secretName)
if err != nil {
return "", err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// Kube secret doesn't exist yet, can't have an authkey.
return "", nil
}
return "", err
ak, ok := s.Data["authkey"]
if !ok {
return "", nil
}
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// We use a map[string]any here rather than import corev1.Secret,
// because we only do very limited things to the secret, and
// importing corev1 adds 12MiB to the compiled binary.
var s map[string]any
if err := json.Unmarshal(bs, &s); err != nil {
return "", err
}
if d, ok := s["data"].(map[string]any); ok {
if v, ok := d["authkey"].(string); ok {
bs, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return "", err
}
return string(bs), nil
}
}
return "", nil
return string(ak), nil
}
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
@@ -145,65 +35,38 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
// First check if the secret exists at all. Even if running on
// kubernetes, we do not necessarily store state in a k8s secret.
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
if err != nil {
return err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
// Assume the secret doesn't exist, or we don't have
// permission to access it.
return nil
if _, err := kc.GetSecret(ctx, secretName); err != nil {
if s, ok := err.(*kube.Status); ok {
if s.Code >= 400 && s.Code <= 499 {
// Assume the secret doesn't exist, or we don't have
// permission to access it.
return nil
}
}
return err
}
m := map[string]map[string]string{
"stringData": {
"device_id": string(deviceID),
"device_fqdn": fqdn,
m := &kube.Secret{
Data: map[string][]byte{
"device_id": []byte(deviceID),
"device_fqdn": []byte(fqdn),
},
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
if _, err := doKubeRequest(ctx, req); err != nil {
return err
}
return nil
return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container")
}
// deleteAuthKey deletes the 'authkey' field of the given kube
// secret. No-op if there is no authkey in the secret.
func deleteAuthKey(ctx context.Context, secretName string) error {
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
m := []struct {
Op string `json:"op"`
Path string `json:"path"`
}{
m := []kube.JSONPatch{
{
Op: "remove",
Path: "/data/authkey",
},
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json-patch+json")
if resp, err := doKubeRequest(ctx, req); err != nil {
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
if err := kc.JSONPatchSecret(ctx, secretName, m); err != nil {
if s, ok := err.(*kube.Status); ok && s.Code == http.StatusUnprocessableEntity {
// This is kubernetes-ese for "the field you asked to
// delete already doesn't exist", aka no-op.
return nil
@@ -213,65 +76,22 @@ func deleteAuthKey(ctx context.Context, secretName string) error {
return nil
}
var (
kubeHost string
kubeNamespace string
kubeToken string
kubeHTTP *http.Transport
)
var kc *kube.Client
func initKube(root string) {
// If running in Kubernetes, set things up so that doKubeRequest
// can talk successfully to the kube apiserver.
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
return
if root != "/" {
// If we are running in a test, we need to set the root path to the fake
// service account directory.
kube.SetRootPathForTesting(root)
}
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
var err error
kc, err = kube.New()
if err != nil {
log.Fatalf("Error reading kube namespace: %v", err)
log.Fatalf("Error creating kube client: %v", err)
}
kubeNamespace = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
if err != nil {
log.Fatalf("Error reading kube token: %v", err)
}
kubeToken = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
if err != nil {
log.Fatalf("Error reading kube CA cert: %v", err)
}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(bs)
kubeHTTP = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: cp,
},
IdleConnTimeout: time.Second,
if root != "/" {
// If we are running in a test, we need to set the URL to the
// httptest server.
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
}
}
// doKubeRequest sends r to the kube apiserver.
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
if kubeHTTP == nil {
panic("not in kubernetes")
}
r.URL.Scheme = "https"
r.URL.Host = kubeHost
r.Header.Set("Authorization", "Bearer "+kubeToken)
r.Header.Set("Accept", "application/json")
resp, err := kubeHTTP.RoundTrip(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
}
return resp, nil
}

View File

@@ -123,7 +123,7 @@ func main() {
defer cancel()
if cfg.InKubernetes && cfg.KubeSecret != "" {
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
canPatch, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
}

View File

@@ -607,7 +607,7 @@ func TestContainerBoot(t *testing.T) {
}()
var wantCmds []string
for _, p := range test.Phases {
for i, p := range test.Phases {
lapi.Notify(p.Notify)
wantCmds = append(wantCmds, p.WantCmds...)
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
@@ -626,7 +626,7 @@ func TestContainerBoot(t *testing.T) {
return nil
})
if err != nil {
t.Fatal(err)
t.Fatalf("phase %d: %v", i, err)
}
err = tstest.WaitFor(2*time.Second, func() error {
for path, want := range p.WantFiles {
@@ -983,13 +983,13 @@ func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
}
case "application/strategic-merge-patch+json":
req := struct {
Data map[string]string `json:"stringData"`
Data map[string][]byte `json:"data"`
}{}
if err := json.Unmarshal(bs, &req); err != nil {
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
}
for key, val := range req.Data {
k.secret[key] = val
k.secret[key] = string(val)
}
default:
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))

View File

@@ -14,6 +14,7 @@ import (
"time"
"tailscale.com/syncs"
"tailscale.com/util/slicesx"
)
const refreshTimeout = time.Minute
@@ -52,6 +53,13 @@ func refreshBootstrapDNS() {
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
defer cancel()
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
// Randomize the order of the IPs for each name to avoid the client biasing
// to IPv6
for k := range dnsEntries {
ips := dnsEntries[k]
slicesx.Shuffle(ips)
dnsEntries[k] = ips
}
j, err := json.MarshalIndent(dnsEntries, "", "\t")
if err != nil {
// leave the old values in place

View File

@@ -11,14 +11,12 @@ import (
"net/url"
"reflect"
"testing"
"tailscale.com/tstest"
)
func BenchmarkHandleBootstrapDNS(b *testing.B) {
prev := *bootstrapDNS
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
defer func() {
*bootstrapDNS = prev
}()
tstest.Replace(b, bootstrapDNS, "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com")
refreshBootstrapDNS()
w := new(bitbucketResponseWriter)
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)

View File

@@ -81,7 +81,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
return &tls.Config{
Certificates: nil,
NextProtos: []string{
"h2", "http/1.1", // enable HTTP/2
"http/1.1",
},
GetCertificate: m.getCertificate,
}

View File

@@ -3,26 +3,69 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
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/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@@ -44,9 +87,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -59,8 +104,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb from tailscale.com/cmd/derper
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -84,8 +131,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health
tailscale.com/util/set from tailscale.com/health
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/vizerror from tailscale.com/tsweb
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
@@ -110,7 +158,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http/httpproxy from net/http+
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+
golang.org/x/net/proxy from tailscale.com/net/netns
@@ -167,8 +215,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
expvar from tailscale.com/cmd/derper+
flag from tailscale.com/cmd/derper
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
io from bufio+
@@ -186,7 +236,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/http from expvar+
net/http/httptrace from net/http+
net/http/internal from net/http
net/http/pprof from tailscale.com/tsweb
net/http/pprof from tailscale.com/tsweb+
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
@@ -199,6 +249,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
regexp from internal/profile+
regexp/syntax from regexp
runtime/debug from golang.org/x/crypto/acme+
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
sort from compress/flate+

View File

@@ -36,8 +36,8 @@ import (
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")

View File

@@ -5,7 +5,6 @@
package main
import (
"expvar"
"flag"
"fmt"
"html"
@@ -23,13 +22,14 @@ var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
spread = flag.Bool("spread", true, "whether to spread probing over time")
interval = flag.Duration("interval", 15*time.Second, "probe interval")
)
func main() {
flag.Parse()
p := prober.New().WithSpread(true).WithOnce(*probeOnce)
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
if err != nil {
log.Fatal(err)
@@ -52,7 +52,6 @@ func main() {
mux := http.NewServeMux()
tsweb.Debugger(mux)
expvar.Publish("derpprobe", p.Expvar())
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
log.Fatal(http.ListenAndServe(*listen, mux))
}

28
cmd/dist/dist.go vendored Normal file
View File

@@ -0,0 +1,28 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The dist command builds Tailscale release packages for distribution.
package main
import (
"context"
"errors"
"flag"
"log"
"os"
"tailscale.com/release/dist"
"tailscale.com/release/dist/cli"
"tailscale.com/release/dist/unixpkgs"
)
func getTargets() ([]dist.Target, error) {
return unixpkgs.Targets(), nil
}
func main() {
cmd := cli.CLI(getTargets)
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
log.Fatal(err)
}
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BSD-3-Clause
// get-authkey allocates an authkey using an OAuth API client
// https://tailscale.com/kb/1215/oauth-clients/ and prints it
// https://tailscale.com/s/oauth-clients and prints it
// to stdout for scripts to capture and use.
package main
@@ -29,9 +29,9 @@ func main() {
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
flag.Parse()
clientId := os.Getenv("TS_API_CLIENT_ID")
clientID := os.Getenv("TS_API_CLIENT_ID")
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
if clientId == "" || clientSecret == "" {
if clientID == "" || clientSecret == "" {
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
}
@@ -39,22 +39,22 @@ func main() {
log.Fatal("at least one tag must be specified")
}
baseUrl := os.Getenv("TS_BASE_URL")
if baseUrl == "" {
baseUrl = "https://api.tailscale.com"
baseURL := os.Getenv("TS_BASE_URL")
if baseURL == "" {
baseURL = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientId,
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: baseUrl + "/api/v2/oauth/token",
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
ctx := context.Background()
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseUrl
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{

View File

@@ -22,6 +22,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/tailscale/hujson"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/util/httpm"
)
@@ -42,9 +43,9 @@ func modifiedExternallyError() {
}
}
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
if err != nil {
return err
}
@@ -73,7 +74,7 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
return nil
}
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
if err := applyNewACL(ctx, client, tailnet, apiKey, *policyFname, controlEtag); err != nil {
return err
}
@@ -83,9 +84,9 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
}
}
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
if err != nil {
return err
}
@@ -113,16 +114,16 @@ func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
return nil
}
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
if err := testNewACLs(ctx, client, tailnet, apiKey, *policyFname); err != nil {
return err
}
return nil
}
}
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
func getChecksums(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
if err != nil {
return err
}
@@ -151,8 +152,24 @@ func main() {
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
}
apiKey, ok := os.LookupEnv("TS_API_KEY")
if !ok {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
oauthId, oiok := os.LookupEnv("TS_OAUTH_ID")
oauthSecret, osok := os.LookupEnv("TS_OAUTH_SECRET")
if !ok && (!oiok || !osok) {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key or TS_OAUTH_ID and TS_OAUTH_SECRET to your Tailscale OAuth ID and Secret")
}
if ok && (oiok || osok) {
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
}
var client *http.Client
if oiok {
oauthConfig := &clientcredentials.Config{
ClientID: oauthId,
ClientSecret: oauthSecret,
TokenURL: fmt.Sprintf("https://%s/api/v2/oauth/token", *apiServer),
}
client = oauthConfig.Client(context.Background())
} else {
client = http.DefaultClient
}
cache, err := LoadCache(*cacheFname)
if err != nil {
@@ -169,7 +186,7 @@ func main() {
ShortUsage: "gitops-pusher [options] apply",
ShortHelp: "Pushes changes to CONTROL",
LongHelp: `Pushes changes to CONTROL`,
Exec: apply(cache, tailnet, apiKey),
Exec: apply(cache, client, tailnet, apiKey),
}
testCmd := &ffcli.Command{
@@ -177,7 +194,7 @@ func main() {
ShortUsage: "gitops-pusher [options] test",
ShortHelp: "Tests ACL changes",
LongHelp: "Tests ACL changes",
Exec: test(cache, tailnet, apiKey),
Exec: test(cache, client, tailnet, apiKey),
}
cksumCmd := &ffcli.Command{
@@ -185,7 +202,7 @@ func main() {
ShortUsage: "Shows checksums of ACL files",
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
Exec: getChecksums(cache, tailnet, apiKey),
Exec: getChecksums(cache, client, tailnet, apiKey),
}
root := &ffcli.Command{
@@ -228,7 +245,7 @@ func sumFile(fname string) (string, error) {
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname, oldEtag string) error {
fin, err := os.Open(policyFname)
if err != nil {
return err
@@ -244,7 +261,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
req.Header.Set("Content-Type", "application/hujson")
req.Header.Set("If-Match", `"`+oldEtag+`"`)
resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return err
}
@@ -265,7 +282,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
return nil
}
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname string) error {
data, err := os.ReadFile(policyFname)
if err != nil {
return err
@@ -283,7 +300,7 @@ func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error
req.SetBasicAuth(apiKey, "")
req.Header.Set("Content-Type", "application/hujson")
resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return err
}
@@ -346,7 +363,7 @@ type ACLTestErrorDetail struct {
Errors []string `json:"errors"`
}
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
if err != nil {
return "", err
@@ -355,7 +372,7 @@ func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
req.SetBasicAuth(apiKey, "")
req.Header.Set("Accept", "application/hujson")
resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return "", err
}

View File

@@ -7,7 +7,7 @@ metadata:
name: tailscale-auth-proxy
rules:
- apiGroups: [""]
resources: ["users"]
resources: ["users", "groups"]
verbs: ["impersonate"]
---
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -7,8 +7,10 @@ package main
import (
"context"
"crypto/tls"
_ "embed"
"fmt"
"net/http"
"os"
"strings"
"time"
@@ -25,7 +27,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -39,10 +41,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/store/kubestore"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/dnsname"
)
@@ -61,7 +65,7 @@ func main() {
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultEnv("AUTH_PROXY", "false")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
)
var opts []kzap.Opts
@@ -95,6 +99,13 @@ func main() {
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(context.Background())
if shouldRunAuthProxy {
hostinfo.SetApp("k8s-operator-proxy")
} else {
hostinfo.SetApp("k8s-operator")
}
s := &tsnet.Server{
Hostname: hostname,
Logf: zlog.Named("tailscaled").Debugf,
@@ -157,7 +168,7 @@ waitOnline:
loginDone = true
case "NeedsMachineAuth":
if !machineAuthShown {
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
startlog.Infof("Machine approval required, please visit the admin panel to approve")
machineAuthShown = true
}
default:
@@ -225,16 +236,26 @@ waitOnline:
}
startlog.Infof("Startup complete, operator running")
if shouldRunAuthProxy == "true" {
rc, err := rest.TransportFor(restConfig)
if shouldRunAuthProxy {
cfg, err := restConfig.TransportConfig()
if err != nil {
startlog.Fatalf("could not get rest transport: %v", err)
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
authProxyListener, err := s.Listen("tcp", ":443")
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
if err != nil {
startlog.Fatalf("could not listen on :443: %v", err)
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
}
go runAuthProxy(lc, authProxyListener, rc, zlog.Named("auth-proxy").Infof)
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
}
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
startlog.Fatalf("could not start manager: %v", err)
@@ -696,6 +717,15 @@ func getSingleObject[T any, O ptrObject[T]](ctx context.Context, c client.Client
return ret, nil
}
func defaultBool(envName string, defVal bool) bool {
vs := os.Getenv(envName)
if vs == "" {
return defVal
}
v, _ := opt.Bool(vs).Get()
return v
}
func defaultEnv(envName, defVal string) string {
v := os.Getenv(envName)
if v == "" {

View File

@@ -14,7 +14,6 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -670,11 +669,11 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
},
},
},
Containers: []v1.Container{
Containers: []corev1.Container{
{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []v1.EnvVar{
Env: []corev1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_DEST_IP", Value: "10.20.30.40"},

View File

@@ -8,7 +8,6 @@ import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -17,6 +16,7 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
)
@@ -41,23 +41,42 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.rp.ServeHTTP(w, r)
}
func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripper, logf logger.Logf) {
// runAuthProxy runs an HTTP server that authenticates requests using the
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
// It listens on :443 and uses the Tailscale HTTPS certificate.
// s will be started if it is not already running.
// rt is used to proxy requests to the Kubernetes API.
//
// It never returns.
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
ln, err := s.Listen("tcp", ":443")
if err != nil {
log.Fatalf("could not listen on :443: %v", err)
}
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
if err != nil {
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
}
lc, err := s.LocalClient()
if err != nil {
log.Fatalf("could not get local client: %v", err)
}
ap := &authProxy{
logf: logf,
lc: lc,
rp: &httputil.ReverseProxy{
Director: func(r *http.Request) {
// Replace the request with the user's identity.
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
// We want to proxy to the Kubernetes API, but we want to use
// the caller's identity to do so. We do this by impersonating
// the caller using the Kubernetes User Impersonation feature:
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
// Remove all authentication headers.
// Out of paranoia, remove all authentication headers that might
// have been set by the client.
r.Header.Del("Authorization")
r.Header.Del("Impersonate-Group")
r.Header.Del("Impersonate-User")
r.Header.Del("Impersonate-Uid")
for k := range r.Header {
if strings.HasPrefix(k, "Impersonate-Extra-") {
@@ -65,6 +84,19 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
}
}
// Now add the impersonation headers that we want.
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
if who.Node.IsTagged() {
// Use the nodes FQDN as the username, and the nodes tags as the groups.
// "Impersonate-Group" requires "Impersonate-User" to be set.
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
for _, tag := range who.Node.Tags {
r.Header.Add("Impersonate-Group", tag)
}
} else {
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
}
// Replace the URL with the Kubernetes APIServer.
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
@@ -72,9 +104,17 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
Transport: rt,
},
}
if err := http.Serve(tls.NewListener(ls, &tls.Config{
GetCertificate: lc.GetCertificate,
}), ap); err != nil {
hs := &http.Server{
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
TLSConfig: &tls.Config{
GetCertificate: lc.GetCertificate,
NextProtos: []string{"http/1.1"},
},
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: ap,
}
if err := hs.ServeTLS(ln, "", ""); err != nil {
log.Fatalf("runAuthProxy: failed to serve %v", err)
}
}

View File

@@ -19,7 +19,7 @@ func main() {
arch := winres.Arch(os.Args[1])
switch arch {
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
default:
log.Fatalf("unsupported arch: %s", arch)
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// mkversion gets version info from git and outputs a bunch of shell variables
// that get used elsewhere in the build system to embed version numbers into
// binaries.
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"time"
"tailscale.com/tailcfg"
"tailscale.com/version/mkversion"
)
func main() {
prefix := ""
if len(os.Args) > 1 {
if os.Args[1] == "--export" {
prefix = "export "
} else {
fmt.Println("usage: mkversion [--export|-h|--help]")
os.Exit(1)
}
}
var b bytes.Buffer
io.WriteString(&b, mkversion.Info().String())
// Copyright and the client capability are not part of the version
// information, but similarly used in Xcode builds to embed in the metadata,
// thus generate them now.
copyright := fmt.Sprintf("Copyright © %d Tailscale Inc. All Rights Reserved.", time.Now().Year())
fmt.Fprintf(&b, "VERSION_COPYRIGHT=%q\n", copyright)
fmt.Fprintf(&b, "VERSION_CAPABILITY=%d\n", tailcfg.CurrentCapabilityVersion)
s := bufio.NewScanner(&b)
for s.Scan() {
fmt.Println(prefix + s.Text())
}
}

View File

@@ -43,7 +43,7 @@ import (
jsonv2 "github.com/go-json-experiment/json"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/logtail"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/must"
)
@@ -136,8 +136,8 @@ func processObject(dec *jsonv2.Decoder) {
type message struct {
Logtail struct {
ID logtail.PublicID `json:"id"`
Logged time.Time `json:"server_time"`
ID logid.PublicID `json:"id"`
Logged time.Time `json:"server_time"`
} `json:"logtail"`
Logged time.Time `json:"logged"`
netlogtype.Message

View File

@@ -2,30 +2,31 @@
set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.3
for ARCH in amd64 arm64; do
CGO_ENABLED=0 GOARCH=${ARCH} GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.2
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-${ARCH}.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--arch=${ARCH} \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--arch=amd64 \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=amd64 \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-${ARCH}.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=${ARCH} \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
done

View File

@@ -56,7 +56,7 @@ func main() {
return
}
if len(info.Node.Tags) != 0 {
if info.Node.IsTagged() {
w.WriteHeader(http.StatusForbidden)
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
return

View File

@@ -272,7 +272,7 @@ func (p *proxy) serve(sessionID int64, c net.Conn) error {
}
if buf[0] != 'S' {
p.errors.Add("upstream-bad-protocol", 1)
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
return fmt.Errorf("upstream didn't acknowledge start-ssl, said %q", buf[0])
}
tlsConf := &tls.Config{
ServerName: p.upstreamHost,

View File

@@ -147,7 +147,7 @@ func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, i
if err != nil {
return nil, fmt.Errorf("failed to identify remote host: %w", err)
}
if len(whois.Node.Tags) != 0 {
if whois.Node.IsTagged() {
return nil, fmt.Errorf("tagged nodes are not users")
}
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {

222
cmd/sniproxy/sniproxy.go Normal file
View File

@@ -0,0 +1,222 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The sniproxy is an outbound SNI proxy. It receives TLS connections over
// Tailscale on one or more TCP ports and sends them out to the same SNI
// hostname & port on the internet. It only does TCP.
package main
import (
"context"
"flag"
"log"
"net"
"net/http"
"strings"
"time"
"golang.org/x/net/dns/dnsmessage"
"inet.af/tcpproxy"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/net/netutil"
"tailscale.com/tsnet"
"tailscale.com/types/nettype"
)
var (
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
)
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
func main() {
flag.Parse()
if *ports == "" {
log.Fatal("no ports")
}
hostinfo.SetApp("sniproxy")
var s server
defer s.ts.Close()
lc, err := s.ts.LocalClient()
if err != nil {
log.Fatal(err)
}
s.lc = lc
for _, portStr := range strings.Split(*ports, ",") {
ln, err := s.ts.Listen("tcp", ":"+portStr)
if err != nil {
log.Fatal(err)
}
log.Printf("Serving on port %v ...", portStr)
go s.serve(ln)
}
ln, err := s.ts.Listen("udp", ":53")
if err != nil {
log.Fatal(err)
}
go s.serveDNS(ln)
if *promoteHTTPS {
ln, err := s.ts.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
log.Printf("Promoting HTTP to HTTPS ...")
go s.promoteHTTPS(ln)
}
select {}
}
type server struct {
ts tsnet.Server
lc *tailscale.LocalClient
}
func (s *server) serve(ln net.Listener) {
for {
c, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
go s.serveConn(c)
}
}
func (s *server) serveDNS(ln net.Listener) {
for {
c, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
go s.serveDNSConn(c.(nettype.ConnPacketConn))
}
}
func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
defer c.Close()
c.SetReadDeadline(time.Now().Add(5 * time.Second))
buf := make([]byte, 1500)
n, err := c.Read(buf)
if err != nil {
log.Printf("c.Read failed: %v\n ", err)
return
}
var msg dnsmessage.Message
err = msg.Unpack(buf[:n])
if err != nil {
log.Printf("dnsmessage unpack failed: %v\n ", err)
return
}
buf, err = s.dnsResponse(&msg)
if err != nil {
log.Printf("s.dnsResponse failed: %v\n", err)
return
}
_, err = c.Write(buf)
if err != nil {
log.Printf("c.Write failed: %v\n", err)
return
}
}
func (s *server) serveConn(c net.Conn) {
addrPortStr := c.LocalAddr().String()
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("bogus addrPort %q", addrPortStr)
c.Close()
return
}
var dialer net.Dialer
dialer.Timeout = 5 * time.Second
var p tcpproxy.Proxy
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
return netutil.NewOneConnListener(c, nil), nil
}
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
return &tcpproxy.DialProxy{
Addr: net.JoinHostPort(sniName, port),
DialContext: dialer.DialContext,
}, true
})
p.Start()
}
func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
resp := dnsmessage.NewBuilder(buf,
dnsmessage.Header{
ID: req.Header.ID,
Response: true,
Authoritative: true,
})
resp.EnableCompression()
if len(req.Questions) == 0 {
buf, _ = resp.Finish()
return
}
q := req.Questions[0]
err = resp.StartQuestions()
if err != nil {
return
}
resp.Question(q)
ip4, ip6 := s.ts.TailscaleIPs()
err = resp.StartAnswers()
if err != nil {
return
}
switch q.Type {
case dnsmessage.TypeAAAA:
err = resp.AAAAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.AAAAResource{AAAA: ip6.As16()},
)
case dnsmessage.TypeA:
err = resp.AResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.AResource{A: ip4.As4()},
)
case dnsmessage.TypeSOA:
err = resp.SOAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
)
case dnsmessage.TypeNS:
err = resp.NSResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.NSResource{NS: tsMBox},
)
}
if err != nil {
return
}
return resp.Finish()
}
func (s *server) promoteHTTPS(ln net.Listener) {
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
}))
log.Fatalf("promoteHTTPS http.Serve: %v", err)
}

View File

@@ -113,12 +113,15 @@ change in the future.
loginCmd,
logoutCmd,
switchCmd,
configureCmd,
netcheckCmd,
ipCmd,
statusCmd,
pingCmd,
ncCmd,
sshCmd,
funnelCmd,
serveCmd,
versionCmd,
webCmd,
fileCmd,
@@ -146,12 +149,8 @@ change in the future.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "serve"):
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
case slices.Contains(args, "update"):
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
case slices.Contains(args, "configure"):
rootCmd.Subcommands = append(rootCmd.Subcommands, configureCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)

View File

@@ -621,9 +621,16 @@ func TestPrefsFromUpArgs(t *testing.T) {
{
name: "error_long_hostname",
args: upArgsT{
hostname: strings.Repeat("a", 300),
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
},
wantErr: `hostname too long: 300 bytes (max 256)`,
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
},
{
name: "error_long_label",
args: upArgsT{
hostname: strings.Repeat("a", 64) + ".example.com",
},
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
},
{
name: "error_linux_netfilter_empty",
@@ -1071,13 +1078,42 @@ func TestUpdatePrefs(t *testing.T) {
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "force_reauth_over_ssh_no_risk",
flags: []string{"--force-reauth"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "force_reauth_over_ssh",
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: nil,
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.sshOverTailscale {
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
} else if isSSHOverTailscale() {
// The test is being executed over a "real" tailscale SSH
// session, but sshOverTailscale is unset. Make the test appear
// as if it's not over tailscale SSH.
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
}
if tt.env.goos == "" {
tt.env.goos = "linux"

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/exp/slices"
"k8s.io/client-go/util/homedir"
"sigs.k8s.io/yaml"
"tailscale.com/version"
)
func init() {
@@ -25,12 +26,14 @@ func init() {
var configureKubeconfigCmd = &ffcli.Command{
Name: "kubeconfig",
ShortHelp: "Configure kubeconfig to use Tailscale",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortUsage: "kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(`
Run this command to configure your kubeconfig to use Tailscale for authentication to a Kubernetes cluster.
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
See: https://tailscale.com/s/k8s-auth-proxy
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("kubeconfig")
@@ -39,6 +42,22 @@ The hostname argument should be set to the Tailscale hostname of the peer runnin
Exec: runConfigureKubeconfig,
}
// kubeconfigPath returns the path to the kubeconfig file for the current user.
func kubeconfigPath() string {
var dir string
if version.IsSandboxedMacOS() {
// The HOME environment variable in macOS sandboxed apps is set to
// ~/Library/Containers/<app-id>/Data, but the kubeconfig file is
// located in ~/.kube/config. We rely on the "com.apple.security.temporary-exception.files.home-relative-path.read-write"
// entitlement to access the file.
containerHome := os.Getenv("HOME")
dir, _, _ = strings.Cut(containerHome, "/Library/Containers/")
} else {
dir = homedir.HomeDir()
}
return filepath.Join(dir, ".kube", "config")
}
func runConfigureKubeconfig(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("unknown arguments")
@@ -57,8 +76,7 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
return fmt.Errorf("no peer found with hostname %q", hostOrFQDN)
}
targetFQDN = strings.TrimSuffix(targetFQDN, ".")
confPath := filepath.Join(homedir.HomeDir(), ".kube", "config")
if err := setKubeconfigForPeer(targetFQDN, confPath); err != nil {
if err := setKubeconfigForPeer(targetFQDN, kubeconfigPath()); err != nil {
return err
}
printf("kubeconfig configured for %q\n", hostOrFQDN)
@@ -140,9 +158,23 @@ func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
}
func setKubeconfigForPeer(fqdn, filePath string) error {
dir := filepath.Dir(filePath)
if _, err := os.Stat(dir); err != nil {
if !os.IsNotExist(err) {
return err
}
if err := os.Mkdir(dir, 0755); err != nil {
if version.IsSandboxedMacOS() && errors.Is(err, os.ErrPermission) {
// macOS sandboxing prevents us from creating the .kube directory
// in the home directory.
return errors.New("unable to create .kube directory in home directory, please create it manually (e.g. mkdir ~/.kube")
}
return err
}
}
b, err := os.ReadFile(filePath)
if err != nil && !os.IsNotExist(err) {
return err
return fmt.Errorf("reading kubeconfig: %w", err)
}
b, err = updateKubeconfig(b, fqdn)
if err != nil {

View File

@@ -35,13 +35,13 @@ var configureHostCmd = &ffcli.Command{
var synologyConfigureCmd = &ffcli.Command{
Name: "synology",
Exec: runConfigureSynology,
ShortHelp: "Configure Synology to enable more Tailscale features",
ShortHelp: "Configure Synology to enable outbound connections",
LongHelp: strings.TrimSpace(`
The 'configure-host' command is intended to run at boot as root
to create the /dev/net/tun device and give the tailscaled binary
permission to use it.
This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission
to use it.
See: https://tailscale.com/kb/1152/synology-outbound/
See: https://tailscale.com/s/synology-outbound
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("synology")

View File

@@ -15,10 +15,10 @@ import (
var configureCmd = &ffcli.Command{
Name: "configure",
ShortHelp: "Configure the host to enable more Tailscale features",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure' command is intended to provide a way to configure different
services on the host to enable more Tailscale features.
The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways.
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure")

View File

@@ -201,6 +201,23 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "portmap",
Exec: debugPortmap,
ShortHelp: "run portmap debugging debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
fs.StringVar(&debugPortmapArgs.gwSelf, "gw-self", "", `override gateway and self IP (format: "gatewayIP/selfIP")`)
return fs
})(),
},
{
Name: "peer-endpoint-changes",
Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes",
},
},
}
@@ -789,3 +806,82 @@ func runCapture(ctx context.Context, args []string) error {
_, err = io.Copy(f, stream)
return err
}
var debugPortmapArgs struct {
duration time.Duration
gwSelf string
ty string
}
func debugPortmap(ctx context.Context, args []string) error {
rc, err := localClient.DebugPortmap(ctx,
debugPortmapArgs.duration,
debugPortmapArgs.ty,
debugPortmapArgs.gwSelf,
)
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(os.Stdout, rc)
return err
}
func runPeerEndpointChanges(ctx context.Context, args []string) error {
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
if len(args) != 1 || args[0] == "" {
return errors.New("usage: peer-status <hostname-or-IP>")
}
var ip string
hostOrIP := args[0]
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
if err != nil {
return err
}
if self {
printf("%v is local Tailscale IP\n", ip)
return nil
}
if ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/debug-peer-endpoint-changes?ip="+ip, nil)
if err != nil {
return err
}
resp, err := localClient.DoLocalRequest(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var dst bytes.Buffer
if err := json.Indent(&dst, body, "", " "); err != nil {
return fmt.Errorf("indenting returned JSON: %w", err)
}
if ss := dst.String(); !strings.HasSuffix(ss, "\n") {
dst.WriteByte('\n')
}
fmt.Printf("%s", dst.String())
return nil
}

View File

@@ -66,7 +66,7 @@ func isSystemdSystem() bool {
return false
}
switch distro.Get() {
case distro.QNAP, distro.Gokrazy, distro.Synology:
case distro.QNAP, distro.Gokrazy, distro.Synology, distro.Unraid:
return false
}
_, err := exec.LookPath("systemctl")

138
cmd/tailscale/cli/funnel.go Normal file
View File

@@ -0,0 +1,138 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"flag"
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/util/mak"
)
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
// The funnel subcommand is used to turn on/off the Funnel service.
// Funnel is off by default.
// Funnel allows you to publish a 'tailscale serve' server publicly, open to the
// entire internet.
// newFunnelCommand shares the same serveEnv as the "serve" subcommand. See
// newServeCommand and serve.go for more details.
func newFunnelCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
`),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
"",
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
Exec: e.runFunnel,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
},
}
}
// runFunnel is the entry point for the "tailscale funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
if len(args) != 2 {
return flag.ErrHelp
}
var on bool
switch args[1] {
case "on", "off":
on = args[1] == "on"
default:
return flag.ErrHelp
}
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
port64, err := strconv.ParseUint(args[0], 10, 16)
if err != nil {
return err
}
port := uint16(port64)
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
return err
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
if on == sc.AllowFunnel[hp] {
printFunnelWarning(sc)
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
printFunnelWarning(sc)
return nil
}
// printFunnelWarning prints a warning if the Funnel is on but there is no serve
// config for its host:port.
func printFunnelWarning(sc *ipn.ServeConfig) {
var warn bool
for hp, a := range sc.AllowFunnel {
if !a {
continue
}
_, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
warn = true
fmt.Fprintf(os.Stderr, "Warning: funnel=on for %s, but no serve config\n", hp)
}
}
if warn {
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
}
}

View File

@@ -19,6 +19,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
@@ -45,9 +46,15 @@ var netcheckArgs struct {
}
func runNetcheck(ctx context.Context, args []string) error {
logf := logger.WithPrefix(log.Printf, "portmap: ")
netMon, err := netmon.New(logf)
if err != nil {
return err
}
c := &netcheck.Client{
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
PortMapper: portmapper.NewClient(logf, netMon, nil, nil),
UseDNSCache: false, // always resolve, don't cache
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
@@ -96,7 +103,6 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
var err error
switch netcheckArgs.format {
case "":
break
case "json":
j, err = json.MarshalIndent(report, "", "\t")
case "json-line":

View File

@@ -15,6 +15,7 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
@@ -40,7 +41,16 @@ var netlockCmd = &ffcli.Command{
nlLogCmd,
nlLocalDisableCmd,
},
Exec: runNetworkLockStatus,
Exec: runNetworkLockNoSubcommand,
}
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
// Detect & handle the deprecated command 'lock tskey-wrap'.
if len(args) >= 2 && args[0] == "tskey-wrap" {
return runTskeyWrapCmd(ctx, args[1:])
}
return runNetworkLockStatus(ctx, args)
}
var nlInitArgs struct {
@@ -230,6 +240,15 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
if k.Key == st.PublicKey {
line.WriteString("(self)")
}
if k.Metadata["purpose"] == "pre-auth key" {
if preauthKeyID := k.Metadata["authkey_stableid"]; preauthKeyID != "" {
line.WriteString("(pre-auth key ")
line.WriteString(preauthKeyID)
line.WriteString(")")
} else {
line.WriteString("(pre-auth key)")
}
}
fmt.Println(line.String())
}
}
@@ -245,11 +264,13 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
for i, addr := range p.TailscaleIPs {
line.WriteString(addr.String())
if i < len(p.TailscaleIPs)-1 {
line.WriteString(", ")
line.WriteString(",")
}
}
line.WriteString("\t")
line.WriteString(string(p.StableID))
line.WriteString("\t")
line.WriteString(p.NodeKey.String())
fmt.Println(line.String())
}
}
@@ -267,14 +288,78 @@ var nlAddCmd = &ffcli.Command{
},
}
var nlRemoveArgs struct {
resign bool
}
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove <public-key>...",
ShortUsage: "remove [--re-sign=false] <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, nil, args)
},
Exec: runNetworkLockRemove,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock remove")
fs.BoolVar(&nlRemoveArgs.resign, "re-sign", true, "resign signatures which would be invalidated by removal of trusted signing keys")
return fs
})(),
}
func runNetworkLockRemove(ctx context.Context, args []string) error {
removeKeys, _, err := parseNLArgs(args, true, false)
if err != nil {
return err
}
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
if !st.Enabled {
return errors.New("tailnet lock is not enabled")
}
if nlRemoveArgs.resign {
// Validate we are not removing trust in ourselves while resigning. This is because
// we resign with our own key, so the signatures would be immediately invalid.
for _, k := range removeKeys {
kID, err := k.ID()
if err != nil {
return fmt.Errorf("computing KeyID for key %v: %w", k, err)
}
if bytes.Equal(st.PublicKey.KeyID(), kID) {
return errors.New("cannot remove local trusted signing key while resigning; run command on a different node or with --re-sign=false")
}
}
// Resign affected signatures for each of the keys we are removing.
for _, k := range removeKeys {
kID, _ := k.ID() // err already checked above
sigs, err := localClient.NetworkLockAffectedSigs(ctx, kID)
if err != nil {
return fmt.Errorf("affected sigs for key %X: %w", kID, err)
}
for _, sigBytes := range sigs {
var sig tka.NodeKeySignature
if err := sig.Unserialize(sigBytes); err != nil {
return fmt.Errorf("failed decoding signature: %w", err)
}
var nodeKey key.NodePublic
if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
return fmt.Errorf("failed decoding pubkey for signature: %w", err)
}
// Safety: NetworkLockAffectedSigs() verifies all signatures before
// successfully returning.
rotationKey, _ := sig.UnverifiedWrappingPublic()
if err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey)); err != nil {
return fmt.Errorf("failed to sign %v: %w", nodeKey, err)
}
}
}
}
return localClient.NetworkLockModify(ctx, nil, removeKeys)
}
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
@@ -350,13 +435,19 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>]",
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
LongHelp: "Signs a node key and transmits the signature to the coordination server",
Exec: runNetworkLockSign,
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
Exec: runNetworkLockSign,
}
func runNetworkLockSign(ctx context.Context, args []string) error {
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
return runTskeyWrapCmd(ctx, args)
}
var (
nodeKey key.NodePublic
rotationKey key.NLPublic
@@ -558,3 +649,56 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
}
return nil
}
func runTskeyWrapCmd(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
}
if strings.Contains(args[0], "--TL") {
return errors.New("Error: provided key was already wrapped")
}
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
return wrapAuthKey(ctx, args[0], st)
}
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
// Generate a separate tailnet-lock key just for the credential signature.
// We use the free-form meta strings to mark a little bit of metadata about this
// key.
priv := key.NewNLPrivate()
m := map[string]string{
"purpose": "pre-auth key",
"wrapper_stableid": string(status.Self.ID),
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
}
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
// We don't want to accidentally embed the nonce part of the authkey in
// the event the format changes. As such, we make sure its in the format we
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
// out and embed the stableID.
s := strings.TrimPrefix(keyStr, "tskey-auth-")
m["authkey_stableid"] = s[:strings.Index(s, "-")]
}
k := tka.Key{
Kind: tka.Key25519,
Public: priv.Public().Verifier(),
Votes: 1,
Meta: m,
}
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
if err != nil {
return fmt.Errorf("wrapping failed: %w", err)
}
if err := localClient.NetworkLockModify(ctx, []tka.Key{k}, nil); err != nil {
return fmt.Errorf("add key failed: %w", err)
}
fmt.Println(wrapped)
return nil
}

View File

@@ -16,15 +16,14 @@ import (
"path"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/slices"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -35,80 +34,59 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "[ALPHA] Serve from your Tailscale node",
ShortHelp: "Serve content and local servers",
ShortUsage: strings.TrimSpace(`
serve [flags] <mount-point> {proxy|path|text} <arg>
serve [flags] <sub-command> [sub-flags] <args>`),
serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
`),
LongHelp: strings.TrimSpace(`
*** ALPHA; all of this is subject to change ***
*** BETA; all of this is subject to change ***
The 'tailscale serve' set of commands allows you to serve
content and local servers from your Tailscale node to
your tailnet.
your tailnet.
You can also choose to enable the Tailscale Funnel with:
'tailscale serve funnel on'. Funnel allows you to publish
'tailscale funnel on'. Funnel allows you to publish
a 'tailscale serve' server publicly, open to the entire
internet. See https://tailscale.com/funnel.
EXAMPLES
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale serve / proxy 3000
$ tailscale serve https:443 / http://127.0.0.1:3000
Or, using the default port:
$ tailscale serve https / http://127.0.0.1:3000
- To serve a single file or a directory of files:
$ tailscale serve / path /home/alice/blog/index.html
$ tailscale serve /images/ path /home/alice/blog/images
$ tailscale serve https / /home/alice/blog/index.html
$ tailscale serve https /images/ /home/alice/blog/images
- To serve simple static text:
$ tailscale serve / text "Hello, world!"
$ tailscale serve https:8080 / text:"Hello, world!"
- To forward incoming TCP connections on port 2222 to a local TCP server on
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
$ tailscale serve tcp:2222 tcp://localhost:22
- To accept TCP TLS connections (terminated within tailscaled) proxied to a
local plaintext server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
`),
Exec: e.runServe,
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
}),
Exec: e.runServe,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve status",
ShortHelp: "show current serve/funnel status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "tcp",
Exec: e.runServeTCP,
ShortHelp: "add or remove a TCP port forward",
LongHelp: strings.Join([]string{
"EXAMPLES",
" - Forward TLS over TCP to a local TCP server on port 5432:",
" $ tailscale serve tcp 5432",
"",
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
" $ tailscale serve tcp --terminate-tls 5432",
}, "\n"),
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
}),
UsageFunc: usageFunc,
},
{
Name: "funnel",
Exec: e.runServeFunnel,
ShortUsage: "funnel [flags] {on|off}",
ShortHelp: "turn Tailscale Funnel on or off",
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
"",
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
UsageFunc: usageFunc,
},
},
}
}
@@ -145,10 +123,7 @@ type localServeClient interface {
// It also contains the flags, as registered with newServeCommand.
type serveEnv struct {
// flags
servePort uint // Port to serve on. Defaults to 443.
terminateTLS bool
remove bool // remove a serve config
json bool // output JSON (status only for now)
json bool // output JSON (status only for now)
lc localServeClient // localClient interface, specific to serve
@@ -188,28 +163,15 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
return st, nil
}
// validateServePort returns --serve-port flag value,
// or an error if the port is not a valid port to serve on.
func (e *serveEnv) validateServePort() (port uint16, err error) {
// make sure e.servePort is uint16
port = uint16(e.servePort)
if uint(port) != e.servePort {
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
}
// make sure e.servePort is 443, 8443 or 10000
if port != 443 && port != 8443 && port != 10000 {
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
}
return port, nil
}
// runServe is the entry point for the "serve" subcommand, managing Web
// serve config types like proxy, path, and text.
//
// Examples:
// - tailscale serve / proxy 3000
// - tailscale serve /images/ path /var/www/images/
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
// - tailscale serve https / http://localhost:3000
// - tailscale serve https /images/ /var/www/images/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
// - tailscale serve tcp:2222 tcp://localhost:22
// - tailscale serve tls-terminated-tcp:443 tcp://localhost:80
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if len(args) == 0 {
return flag.ErrHelp
@@ -229,39 +191,94 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.lc.SetServeConfig(ctx, sc)
}
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
parsePort := func(portStr string) (uint16, error) {
port64, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, err
}
return uint16(port64), nil
}
srcType, srcPortStr, found := strings.Cut(args[0], ":")
if !found {
if srcType == "https" && srcPortStr == "" {
// Default https port to 443.
srcPortStr = "443"
} else {
return flag.ErrHelp
}
}
turnOff := "off" == args[len(args)-1]
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
mount, err := cleanMountPoint(args[0])
srcPort, err := parsePort(srcPortStr)
if err != nil {
return err
}
if e.remove {
return e.handleWebServeRemove(ctx, mount)
switch srcType {
case "https":
mount, err := cleanMountPoint(args[1])
if err != nil {
return err
}
if turnOff {
return e.handleWebServeRemove(ctx, srcPort, mount)
}
return e.handleWebServe(ctx, srcPort, mount, args[2])
case "tcp", "tls-terminated-tcp":
if turnOff {
return e.handleTCPServeRemove(ctx, srcPort)
}
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
default:
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
return flag.ErrHelp
}
}
// handleWebServe handles the "tailscale serve https:..." subcommand.
// It configures the serve config to forward HTTPS connections to the
// given source.
//
// Examples:
// - tailscale serve https / http://localhost:3000
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
h := new(ipn.HTTPHandler)
switch args[1] {
case "path":
ts, _, _ := strings.Cut(source, ":")
switch {
case ts == "text":
text := strings.TrimPrefix(source, "text:")
if text == "" {
return errors.New("unable to serve; text cannot be an empty string")
}
h.Text = text
case isProxyTarget(source):
t, err := expandProxyTarget(source)
if err != nil {
return err
}
h.Proxy = t
default: // assume path
if version.IsSandboxedMacOS() {
// don't allow path serving for now on macOS (2022-11-15)
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
}
if !filepath.IsAbs(args[2]) {
if !filepath.IsAbs(source) {
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
return flag.ErrHelp
}
fi, err := os.Stat(args[2])
source = filepath.Clean(source)
fi, err := os.Stat(source)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
return flag.ErrHelp
@@ -271,21 +288,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
// for relative file links to work
mount += "/"
}
h.Path = args[2]
case "proxy":
t, err := expandProxyTarget(args[2])
if err != nil {
return err
}
h.Proxy = t
case "text":
if args[2] == "" {
return errors.New("unable to serve; text cannot be an empty string")
}
h.Text = args[2]
default:
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
return flag.ErrHelp
h.Path = source
}
cursc, err := e.lc.GetServeConfig(ctx)
@@ -300,7 +303,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if err != nil {
return err
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if sc.IsTCPForwardingOnPort(srvPort) {
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
@@ -339,12 +342,36 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return nil
}
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
srvPort, err := e.validateServePort()
if err != nil {
return err
// isProxyTarget reports whether source is a valid proxy target.
func isProxyTarget(source string) bool {
if strings.HasPrefix(source, "http://") ||
strings.HasPrefix(source, "https://") ||
strings.HasPrefix(source, "https+insecure://") {
return true
}
srvPortStr := strconv.Itoa(int(srvPort))
// support "localhost:3000", for example
_, portStr, ok := strings.Cut(source, ":")
if ok && allNumeric(portStr) {
return true
}
return false
}
// allNumeric reports whether s only comprises of digits
// and has at least one digit.
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return s != ""
}
// handleWebServeRemove removes a web handler from the serve config.
// The srvPort argument is the serving port and the mount argument is
// the mount point or registered path to remove.
func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mount string) error {
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
@@ -359,9 +386,9 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
if sc.IsTCPForwardingOnPort(srvPort) {
return errors.New("cannot remove web handler; currently serving TCP")
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if !sc.WebHandlerExists(hp, mount) {
return errors.New("error: serve config does not exist")
return errors.New("error: handler does not exist")
}
// delete existing handler, then cascade delete if empty
delete(sc.Web[hp].Handlers, mount)
@@ -386,6 +413,7 @@ func cleanMountPoint(mount string) (string, error) {
if mount == "" {
return "", errors.New("mount point cannot be empty")
}
mount = cleanMinGWPathConversionIfNeeded(mount)
if !strings.HasPrefix(mount, "/") {
mount = "/" + mount
}
@@ -396,18 +424,31 @@ func cleanMountPoint(mount string) (string, error) {
return "", fmt.Errorf("invalid mount point %q", mount)
}
func expandProxyTarget(target string) (string, error) {
if allNumeric(target) {
p, err := strconv.ParseUint(target, 10, 16)
if p == 0 || err != nil {
return "", fmt.Errorf("invalid port %q", target)
}
return "http://127.0.0.1:" + target, nil
// cleanMinGWPathConversionIfNeeded strips the EXEPATH prefix from the given
// path if the path is a MinGW(ish) (Windows) shell arg.
//
// MinGW(ish) (Windows) shells perform POSIX-to-Windows path conversion
// converting the leading "/" of any shell arg to the EXEPATH, which mangles the
// mount point. Strip the EXEPATH prefix if it exists. #7963
//
// "/C:/Program Files/Git/foo" -> "/foo"
func cleanMinGWPathConversionIfNeeded(path string) string {
// Only do this on Windows.
if runtime.GOOS != "windows" {
return path
}
if !strings.Contains(target, "://") {
target = "http://" + target
if _, ok := os.LookupEnv("MSYSTEM"); ok {
exepath := filepath.ToSlash(os.Getenv("EXEPATH"))
path = strings.TrimPrefix(path, exepath)
}
u, err := url.ParseRequestURI(target)
return path
}
func expandProxyTarget(source string) (string, error) {
if !strings.Contains(source, "://") {
source = "http://" + source
}
u, err := url.ParseRequestURI(source)
if err != nil {
return "", fmt.Errorf("parsing url: %w", err)
}
@@ -417,9 +458,14 @@ func expandProxyTarget(target string) (string, error) {
default:
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
}
port, err := strconv.ParseUint(u.Port(), 10, 16)
if port == 0 || err != nil {
return "", fmt.Errorf("invalid port %q: %w", u.Port(), err)
}
host := u.Hostname()
switch host {
// TODO(shayne,bradfitz): do we want to do this?
case "localhost", "127.0.0.1":
host = "127.0.0.1"
default:
@@ -429,19 +475,115 @@ func expandProxyTarget(target string) (string, error) {
if u.Port() != "" {
url += ":" + u.Port()
}
url += u.Path
return url, nil
}
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
// handleTCPServe handles the "tailscale serve tls-terminated-tcp:..." subcommand.
// It configures the serve config to forward TCP connections to the
// given source.
//
// Examples:
// - tailscale serve tcp:2222 tcp://localhost:22
// - tailscale serve tls-terminated-tcp:8443 tcp://localhost:8080
func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort uint16, dest string) error {
var terminateTLS bool
switch srcType {
case "tcp":
terminateTLS = false
case "tls-terminated-tcp":
terminateTLS = true
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
return flag.ErrHelp
}
dstURL, err := url.Parse(dest)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return flag.ErrHelp
}
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return flag.ErrHelp
}
switch host {
case "localhost", "127.0.0.1":
// ok
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
return flag.ErrHelp
}
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
return flag.ErrHelp
}
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
fwdAddr := "127.0.0.1:" + dstPortStr
if sc.IsServingWeb(srcPort) {
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
}
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if terminateTLS {
sc.TCP[srcPort].TerminateTLS = dnsName
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
}
return s != ""
return nil
}
// runServeStatus prints the current serve config.
// handleTCPServeRemove removes the TCP forwarding configuration for the
// given srvPort, or serving port.
func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
if sc.IsServingWeb(src) {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
}
if ph := sc.GetTCPPortHandler(src); ph != nil {
delete(sc.TCP, src)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
return e.lc.SetServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
}
// runServeStatus is the entry point for the "serve status"
// subcommand and prints the current serve config.
//
// Examples:
// - tailscale status
@@ -460,6 +602,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
e.stdout().Write(j)
return nil
}
printFunnelStatus(ctx)
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
printf("No serve config\n")
return nil
@@ -478,17 +621,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
printWebStatusTree(sc, hp)
printf("\n")
}
// warn when funnel on without handlers
for hp, a := range sc.AllowFunnel {
if !a {
continue
}
_, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
}
}
printFunnelWarning(sc)
return nil
}
@@ -572,152 +705,3 @@ func elipticallyTruncate(s string, max int) string {
}
return s[:max-3] + "..."
}
// runServeTCP is the entry point for the "serve tcp" subcommand and
// manages the serve config for TCP forwarding.
//
// Examples:
// - tailscale serve tcp 5432
// - tailscale serve --serve-port=8443 tcp 4430
// - tailscale serve --serve-port=10000 tcp --terminate-tls 8080
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
portStr := args[0]
p, err := strconv.ParseUint(portStr, 10, 16)
if p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
}
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
fwdAddr := "127.0.0.1:" + portStr
if sc.IsServingWeb(srvPort) {
if e.remove {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
}
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
}
if e.remove {
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
delete(sc.TCP, srvPort)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
return e.lc.SetServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if e.terminateTLS {
sc.TCP[srvPort].TerminateTLS = dnsName
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
}
return nil
}
// runServeFunnel is the entry point for the "serve funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
var on bool
switch args[0] {
case "on", "off":
on = args[0] == "on"
default:
return flag.ErrHelp
}
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
if err := checkHasAccess(st.Self.Capabilities); err != nil {
return err
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
if on == sc.AllowFunnel[hp] {
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
return nil
}
// checkHasAccess checks three things: 1) an invite was used to join the
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
// If any of these are false, an error is returned describing the problem.
//
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for Funnel.
func checkHasAccess(nodeAttrs []string) error {
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
}
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
return nil
}

View File

@@ -15,6 +15,7 @@ import (
"strings"
"testing"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
@@ -48,30 +49,6 @@ func TestCleanMountPoint(t *testing.T) {
}
}
func TestCheckHasAccess(t *testing.T) {
tests := []struct {
caps []string
wantErr bool
}{
{[]string{}, true}, // No "funnel" attribute
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
{[]string{tailcfg.NodeAttrFunnel}, false},
}
for _, tt := range tests {
err := checkHasAccess(tt.caps)
switch {
case err != nil && tt.wantErr,
err == nil && !tt.wantErr:
continue
case tt.wantErr:
t.Fatalf("got no error, want error")
case !tt.wantErr:
t.Fatalf("got error %v, want no error", err)
}
}
}
func TestServeConfigMutations(t *testing.T) {
// Stateful mutations, starting from an empty config.
type step struct {
@@ -80,6 +57,8 @@ func TestServeConfigMutations(t *testing.T) {
want *ipn.ServeConfig // non-nil means we want a save of this value
wantErr func(error) (badErrMsg string) // nil means no error is wanted
line int // line number of addStep call, for error messages
debugBreak func()
}
var steps []step
add := func(s step) {
@@ -90,19 +69,19 @@ func TestServeConfigMutations(t *testing.T) {
// funnel
add(step{reset: true})
add(step{
command: cmd("funnel on"),
command: cmd("funnel 443 on"),
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
})
add(step{
command: cmd("funnel on"),
command: cmd("funnel 443 on"),
want: nil, // nothing to save
})
add(step{
command: cmd("funnel off"),
command: cmd("funnel 443 off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("funnel off"),
command: cmd("funnel 443 off"),
want: nil, // nothing to save
})
add(step{
@@ -113,27 +92,23 @@ func TestServeConfigMutations(t *testing.T) {
// https
add(step{reset: true})
add(step{
command: cmd("/ proxy 0"), // invalid port, too low
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy 65536"), // invalid port, too high
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy somehost"), // invalid host
command: cmd("https:443 / http://somehost:3000"), // invalid host
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy http://otherhost"), // invalid host
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy 3000"),
add(step{ // allow omitting port (default to 443)
command: cmd("https / http://localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -143,12 +118,33 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{ // invalid port
command: cmd("--serve-port=9999 /abc proxy 3001"),
wantErr: anyErr(),
add(step{ // support non Funnel port
command: cmd("https:9999 /abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("--serve-port=8443 /abc proxy 3001"),
command: cmd("https:9999 /abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -162,7 +158,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("--serve-port=10000 / text hi"),
command: cmd("https:10000 / text:hi"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
@@ -180,12 +176,12 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("--remove /foo"),
command: cmd("https:443 /foo off"),
want: nil, // nothing to save
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
add(step{
command: cmd("--remove --serve-port=10000 /"),
command: cmd("https:10000 / off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -199,7 +195,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("--remove /"),
command: cmd("https:443 / off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -210,11 +206,11 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("--remove --serve-port=8443 /abc"),
command: cmd("https:8443 /abc off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("bar proxy https://127.0.0.1:8443"),
add(step{ // clean mount: "bar" becomes "/bar"
command: cmd("https:443 bar https://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -225,12 +221,12 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("bar proxy https://127.0.0.1:8443"),
command: cmd("https:443 bar https://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{reset: true})
add(step{
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -242,7 +238,7 @@ func TestServeConfigMutations(t *testing.T) {
})
add(step{reset: true})
add(step{
command: cmd("/foo proxy localhost:3000"),
command: cmd("https:443 /foo localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -253,7 +249,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // test a second handler on the same port
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
command: cmd("https:8443 /foo localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -266,19 +262,50 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{reset: true})
add(step{ // support path in proxy
command: cmd("https / http://127.0.0.1:3000/foo/bar"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
}},
},
},
})
// tcp
add(step{reset: true})
add(step{ // must include scheme for tcp
command: cmd("tls-terminated-tcp:443 localhost:5432"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // !somehost, must be localhost or 127.0.0.1
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // bad target port, too low
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // bad target port, too high
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{
command: cmd("tcp 5432"),
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:5432"},
443: {
TCPForward: "127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{
command: cmd("tcp -terminate-tls 8443"),
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
@@ -289,11 +316,11 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("tcp -terminate-tls 8443"),
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{
command: cmd("tcp --terminate-tls 8444"),
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
@@ -304,35 +331,41 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("tcp -terminate-tls=false 8445"),
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:8445"},
443: {
TCPForward: "127.0.0.1:8445",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{reset: true})
add(step{
command: cmd("tcp 123"),
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:123"},
443: {
TCPForward: "127.0.0.1:123",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{
command: cmd("--remove tcp 321"),
add(step{ // handler doesn't exist, so we get an error
command: cmd("tls-terminated-tcp:8443 off"),
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
})
add(step{
command: cmd("--remove tcp 123"),
command: cmd("tls-terminated-tcp:443 off"),
want: &ipn.ServeConfig{},
})
// text
add(step{reset: true})
add(step{
command: cmd("/ text hello"),
command: cmd("https:443 / text:hello"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -353,7 +386,7 @@ func TestServeConfigMutations(t *testing.T) {
add(step{reset: true})
writeFile("foo", "this is foo")
add(step{
command: cmd("/ path " + filepath.Join(td, "foo")),
command: cmd("https:443 / " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -366,7 +399,7 @@ func TestServeConfigMutations(t *testing.T) {
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
writeFile("subdir/file-a", "this is A")
add(step{
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -377,13 +410,13 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{
command: cmd("/ path missing"),
add(step{ // bad path
command: cmd("https:443 / bad/path"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{
command: cmd("/ path " + filepath.Join(td, "subdir")),
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -394,14 +427,14 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("--remove /"),
command: cmd("https:443 / off"),
want: &ipn.ServeConfig{},
})
// combos
add(step{reset: true})
add(step{
command: cmd("/ proxy 3000"),
command: cmd("https:443 / localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -412,7 +445,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("funnel on"),
command: cmd("funnel 443 on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@@ -424,7 +457,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // serving on secondary port doesn't change funnel
command: cmd("--serve-port=8443 /bar proxy 3001"),
command: cmd("https:8443 /bar localhost:3001"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -439,7 +472,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // turn funnel on for secondary port
command: cmd("--serve-port=8443 funnel on"),
command: cmd("funnel 8443 on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -454,7 +487,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // turn funnel off for primary port 443
command: cmd("funnel off"),
command: cmd("funnel 443 off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -469,7 +502,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // remove secondary port
command: cmd("--serve-port=8443 --remove /bar"),
command: cmd("https:8443 /bar off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@@ -481,7 +514,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // start a tcp forwarder on 8443
command: cmd("--serve-port=8443 tcp 5432"),
command: cmd("tcp:8443 tcp://localhost:5432"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
@@ -493,27 +526,27 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // remove primary port http handler
command: cmd("--remove /"),
command: cmd("https:443 / off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
},
})
add(step{ // remove tcp forwarder
command: cmd("--serve-port=8443 --remove tcp 5432"),
command: cmd("tls-terminated-tcp:8443 off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
},
})
add(step{ // turn off funnel
command: cmd("--serve-port=8443 funnel off"),
command: cmd("funnel 8443 off"),
want: &ipn.ServeConfig{},
})
// tricky steps
add(step{reset: true})
add(step{ // a directory with a trailing slash mount point
command: cmd("/dir path " + filepath.Join(td, "subdir")),
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -524,7 +557,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // this should overwrite the previous one
command: cmd("/dir path " + filepath.Join(td, "foo")),
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -536,7 +569,7 @@ func TestServeConfigMutations(t *testing.T) {
})
add(step{reset: true}) // reset and do the opposite
add(step{ // a file without a trailing slash mount point
command: cmd("/dir path " + filepath.Join(td, "foo")),
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -547,7 +580,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // this should overwrite the previous one
command: cmd("/dir path " + filepath.Join(td, "subdir")),
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -560,37 +593,24 @@ func TestServeConfigMutations(t *testing.T) {
// error states
add(step{reset: true})
add(step{ // make sure we can't add "tcp" as if it was a mount
command: cmd("tcp text foo"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // "/tcp" is fine though as a mount
command: cmd("/tcp text foo"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/tcp": {Text: "foo"},
}},
},
},
})
add(step{reset: true})
add(step{ // tcp forward 5432 on serve port 443
command: cmd("tcp 5432"),
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:5432"},
443: {
TCPForward: "127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{ // try to start a web handler on the same port
command: cmd("/ proxy 3000"),
command: cmd("https:443 / localhost:3000"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{ // start a web handler on port 443
command: cmd("/ proxy 3000"),
command: cmd("https:443 / localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -600,14 +620,17 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
command: cmd("tcp 5432"),
add(step{ // try to start a tcp forwarder on the same serve port
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
wantErr: anyErr(),
})
lc := &fakeLocalServeClient{}
// And now run the steps above.
for i, st := range steps {
if st.debugBreak != nil {
st.debugBreak()
}
if st.reset {
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
lc.config = nil
@@ -625,8 +648,16 @@ func TestServeConfigMutations(t *testing.T) {
testStdout: &stdout,
}
lastCount := lc.setCount
cmd := newServeCommand(e)
err := cmd.ParseAndRun(context.Background(), st.command)
var cmd *ffcli.Command
var args []string
if st.command[0] == "funnel" {
cmd = newFunnelCommand(e)
args = st.command[1:]
} else {
cmd = newServeCommand(e)
args = st.command
}
err := cmd.ParseAndRun(context.Background(), args)
if flagOut.Len() > 0 {
t.Logf("flag package output: %q", flagOut.Bytes())
}
@@ -677,7 +708,7 @@ var fakeStatus = &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel},
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
},
}
@@ -717,7 +748,5 @@ func anyErr() func(error) string {
}
func cmd(s string) []string {
cmds := strings.Fields(s)
fmt.Printf("cmd: %v", cmds)
return cmds
return strings.Fields(s)
}

View File

@@ -258,6 +258,7 @@ func printFunnelStatus(ctx context.Context) {
}
printf("# - %s\n", url)
}
outln()
}
// isRunningOrStarting reports whether st is in state Running or Starting.
@@ -275,7 +276,7 @@ func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
}
return s, false
case ipn.NeedsMachineAuth.String():
return "Machine is not yet authorized by tailnet admin.", false
return "Machine is not yet approved by tailnet admin.", false
case ipn.Running.String(), ipn.Starting.String():
return st.BackendState, true
}

View File

@@ -13,11 +13,13 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"os"
"os/signal"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@@ -26,6 +28,9 @@ import (
shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -34,6 +39,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/dnsname"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -320,8 +326,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
}
}
if len(upArgs.hostname) > 256 {
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
return nil, err
}
prefs := ipn.NewPrefs()
@@ -409,6 +415,12 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return false, nil, err
}
if env.upArgs.forceReauth && isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will result in your SSH session disconnecting.`, env.upArgs.acceptedRisks); err != nil {
return false, nil, err
}
}
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
simpleUp = env.flagSet.NFlag() == 0 &&
@@ -584,7 +596,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
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())
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
case ipn.Running:
// Done full authentication process
@@ -656,6 +668,10 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if err != nil {
return err
}
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
if err != nil {
return err
}
if err := localClient.Start(ctx, ipn.Options{
AuthKey: authKey,
UpdatePrefs: prefs,
@@ -1095,3 +1111,96 @@ func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
}
return false
}
func init() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
}
// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
// property defaults to true if unspecified. The "preauthorized" defaults to
// false. The "baseURL" defaults to https://api.tailscale.com.
// The passed in tags are required, and must be non-empty. These will be
// set on the authkey generated by the OAuth2 dance.
func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
if !strings.HasPrefix(v, "tskey-client-") {
return v, nil
}
if !envknob.Bool("TS_EXPERIMENT_OAUTH_AUTHKEY") {
return "", errors.New("oauth authkeys are in experimental status")
}
if tags == "" {
return "", errors.New("oauth authkeys require --advertise-tags")
}
clientSecret, named, _ := strings.Cut(v, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err := getBool("ephemeral", true)
if err != nil {
return "", err
}
preauth, err := getBool("preauthorized", false)
if err != nil {
return "", err
}
baseURL := "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
credentials := clientcredentials.Config{
ClientID: "some-client-id", // ignored
ClientSecret: clientSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Ephemeral: ephemeral,
Preauthorized: preauth,
Tags: strings.Split(tags, ","),
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return authkey, nil
}

View File

@@ -145,11 +145,11 @@ func newUpdater() (*updater, error) {
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys
default:
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version")
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
}
}
if up.update == nil {
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/")
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
}
return up, nil
}

View File

@@ -61,6 +61,8 @@ type tmplData struct {
TUNMode bool
IsSynology bool
DSMVersion int // 6 or 7, if IsSynology=true
IsUnraid bool
UnraidToken string
IPNVersion string
}
@@ -228,33 +230,48 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
return "", nil, fmt.Errorf("not authenticated by any mechanism")
}
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
// running based on the request URL. This is necessary because QNAP has so
// many options, see https://github.com/tailscale/tailscale/issues/7108
// and https://github.com/tailscale/tailscale/issues/6903
func qnapAuthnURL(requestUrl string, query url.Values) string {
in, err := url.Parse(requestUrl)
scheme := ""
host := ""
if err != nil || in.Scheme == "" {
log.Printf("Cannot parse QNAP login URL %v", err)
// try localhost and hope for the best
scheme = "http"
host = "localhost"
} else {
scheme = in.Scheme
host = in.Host
}
u := url.URL{
Scheme: scheme,
Host: host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return u.String()
}
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
query := url.Values{
"qtoken": []string{token},
"user": []string{user},
}
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
}
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
query := url.Values{
"sid": []string{sid},
}
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
}
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
@@ -425,6 +442,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
LicensesURL: licensesURL(),
TUNMode: st.TUN,
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
IsUnraid: distro.Get() == distro.Unraid,
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
DSMVersion: distro.DSMVersion(),
IPNVersion: versionShort,
}

View File

@@ -26,9 +26,9 @@
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile.LoginName }}
{{ with .Profile }}
<div class="text-right w-full leading-4">
<h4 class="truncate leading-normal">{{.}}</h4>
<h4 class="truncate leading-normal">{{.LoginName}}</h4>
<div class="text-xs text-gray-500 text-right">
<a href="#" class="hover:text-gray-700 js-loginButton">Switch account</a> | <a href="#"
class="hover:text-gray-700 js-loginButton">Reauthenticate</a> | <a href="#"
@@ -116,10 +116,12 @@
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
</footer>
<script>(function () {
const advertiseExitNode = {{.AdvertiseExitNode}};
const advertiseExitNode = {{ .AdvertiseExitNode }};
const isUnraid = {{ .IsUnraid }};
const unraidCsrfToken = "{{ .UnraidToken }}";
let fetchingUrl = false;
var data = {
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
AdvertiseRoutes: "{{ .AdvertiseRoutes }}",
AdvertiseExitNode: advertiseExitNode,
Reauthenticate: false,
ForceLogout: false
@@ -141,15 +143,25 @@ function postData(e) {
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
const url = nextUrl.toString();
let contentType = "application/json";
let body = JSON.stringify(data);
if (isUnraid) {
const params = new URLSearchParams();
params.append("csrf_token", unraidCsrfToken);
params.append("ts_data", JSON.stringify(data));
body = params.toString();
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
}
const url = nextUrl.toString();
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Content-Type": contentType,
},
body: JSON.stringify(data)
body: body
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
@@ -158,7 +170,11 @@ function postData(e) {
}
const url = res["url"];
if (url) {
document.location.href = url;
if(isUnraid) {
window.open(url, "_blank");
} else {
document.location.href = url;
}
} else {
location.reload();
}

View File

@@ -3,7 +3,10 @@
package cli
import "testing"
import (
"net/url"
"testing"
)
func TestUrlOfListenAddr(t *testing.T) {
tests := []struct {
@@ -34,9 +37,64 @@ func TestUrlOfListenAddr(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := urlOfListenAddr(tt.in)
if url != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, url)
u := urlOfListenAddr(tt.in)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
}
})
}
}
func TestQnapAuthnURL(t *testing.T) {
query := url.Values{
"qtoken": []string{"token"},
}
tests := []struct {
name string
in string
want string
}{
{
name: "localhost http",
in: "http://localhost:8088/",
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "localhost https",
in: "https://localhost:5000/",
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP http",
in: "http://10.1.20.4:80/",
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP6 https",
in: "https://[ff7d:0:1:2::1]/",
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "hostname https",
in: "https://qnap.example.com/",
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "invalid URL",
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "err != nil",
in: "http://192.168.0.%31/",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := qnapAuthnURL(tt.in, query)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
}
})
}

View File

@@ -3,7 +3,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
@@ -13,7 +15,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress/flate from nhooyr.io/websocket
@@ -74,11 +76,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/packet from tailscale.com/wgengine/filter+
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
@@ -91,7 +95,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -121,6 +125,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
@@ -149,9 +154,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/icmp from tailscale.com/net/ping
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from golang.org/x/net/icmp+
golang.org/x/net/ipv6 from golang.org/x/net/icmp
golang.org/x/net/ipv6 from golang.org/x/net/icmp+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
golang.org/x/oauth2/internal from golang.org/x/oauth2+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+

View File

@@ -6,4 +6,3 @@ package main
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso

View File

@@ -14,25 +14,19 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"os"
"strings"
"time"
"tailscale.com/derp/derphttp"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/portmapper"
"tailscale.com/net/netmon"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
@@ -48,7 +42,7 @@ var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
@@ -82,7 +76,7 @@ func runMonitor(ctx context.Context, loop bool) error {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := monitor.New(log.Printf)
mon, err := netmon.New(log.Printf)
if err != nil {
return err
}
@@ -90,10 +84,10 @@ func runMonitor(ctx context.Context, loop bool) error {
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Link monitor fired; no change")
log.Printf("Network monitor fired; no change")
return
}
log.Printf("Link monitor fired. New state:")
log.Printf("Network monitor fired. New state:")
dump(st)
})
if loop {
@@ -199,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
priv1 := key.NewNode()
priv2 := key.NewNode()
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
defer func() {
if err != nil {
c1.Close()
@@ -229,95 +223,5 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
}
func debugPortmap(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
logf := log.Printf
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
}
logf("cb: no mapping")
})
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
return err
}
gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netip.MustParseAddr(v[:i])
self = netip.MustParseAddr(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()
}
c.SetGatewayLookupFunc(gatewayAndSelfIP)
gw, selfIP, ok := gatewayAndSelfIP()
if !ok {
logf("no gateway or self IP; %v", linkMon.InterfaceState())
return nil
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
}
logf("Probe: %+v", res)
if !res.PCP && !res.PMP && !res.UPnP {
logf("no portmapping services available")
return nil
}
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {
logf("no mapping")
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
}

View File

@@ -3,7 +3,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
@@ -12,7 +14,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
@@ -38,6 +40,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
L github.com/aws/aws-sdk-go-v2/internal/shareddefaults from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
@@ -48,16 +51,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/ssooidc from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssooidc
L github.com/aws/aws-sdk-go-v2/service/ssooidc/types from github.com/aws/aws-sdk-go-v2/service/ssooidc
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/context from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
@@ -73,6 +79,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
github.com/fxamacker/cbor/v2 from tailscale.com/tka
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
@@ -93,7 +100,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd+
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
@@ -105,12 +112,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
L github.com/pierrec/lz4/v4 from github.com/u-root/uio/uio
L github.com/pierrec/lz4/v4/internal/lz4block from github.com/pierrec/lz4/v4+
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
W github.com/pkg/errors from github.com/tailscale/certstore
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
@@ -154,6 +166,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/bufferv2+
@@ -200,6 +213,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
@@ -212,17 +226,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/control/controlclient+
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/logtail/filch from tailscale.com/logpolicy+
tailscale.com/metrics from tailscale.com/derp+
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
@@ -237,21 +252,24 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netknob from tailscale.com/net/netns+
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/ping from tailscale.com/net/netcheck+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
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+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun/table from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -260,13 +278,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
@@ -298,15 +317,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
tailscale.com/util/vizerror from tailscale.com/tsweb
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
@@ -314,7 +337,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
tailscale.com/wgengine/netlog from tailscale.com/wgengine
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
@@ -340,13 +362,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http/httpproxy 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 golang.org/x/net/http2+
@@ -410,7 +432,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/xml from github.com/tailscale/goupnp+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/control/controlclient+
flag from net/http/httptest+
fmt from compress/flate+
hash from crypto+
hash/adler32 from tailscale.com/ipn/ipnlocal

View File

@@ -6,4 +6,3 @@ package main
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso

View File

@@ -18,7 +18,7 @@ import (
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS, distro.QNAP:
case distro.Synology, distro.TrueNAS, distro.QNAP, distro.Unraid:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
@@ -42,6 +42,8 @@ func findTaildropDir(dg distro.Distro) (string, error) {
return findTrueNASTaildropDir(name)
case distro.QNAP:
return findQnapTaildropDir(name)
case distro.Unraid:
return findUnraidTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
@@ -103,3 +105,25 @@ func findQnapTaildropDir(name string) (string, error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findUnraidTaildropDir looks for a directory linked at
// /var/lib/tailscale/Taildrop. This is a symlink to the
// path specified by the user in the Unraid Web UI
func findUnraidTaildropDir(name string) (string, error) {
dir := fmt.Sprintf("/var/lib/tailscale/%s", name)
_, err := os.Stat(dir)
if err != nil {
return "", fmt.Errorf("symlink %q not found", name)
}
fullpath, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("symlink %q to shared folder not valid", name)
}
fi, err := os.Stat(fullpath)
if err == nil && fi.IsDir() {
return dir, nil // return the symlink
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@@ -39,25 +39,27 @@ import (
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/syncs"
"tailscale.com/tsweb"
"tailscale.com/tsweb/varz"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -327,7 +329,15 @@ var logPol *logpolicy.Policy
var debugMux *http.ServeMux
func run() error {
pol := logpolicy.New(logtail.CollectionNode)
var logf logger.Logf = log.Printf
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}
pol := logpolicy.New(logtail.CollectionNode, netMon)
pol.SetVerbosityLevel(args.verbose)
logPol = pol
defer func() {
@@ -351,7 +361,6 @@ func run() error {
return nil
}
var logf logger.Logf = log.Printf
if envknob.Bool("TS_DEBUG_MEMORY") {
logf = logger.RusagePrefixLog(logf)
}
@@ -377,11 +386,10 @@ func run() error {
debugMux = newDebugMux()
}
logid := pol.PublicID.String()
return startIPNServer(context.Background(), logf, logid)
return startIPNServer(context.Background(), logf, pol.PublicID, netMon)
}
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) error {
ln, err := safesocket.Listen(args.socketpath)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
@@ -407,7 +415,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
}
}()
srv := ipnserver.New(logf, logid)
srv := ipnserver.New(logf, logID, netMon)
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
@@ -425,7 +433,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
return
}
}
lb, err := getLocalBackend(ctx, logf, logid)
lb, err := getLocalBackend(ctx, logf, logID, netMon)
if err == nil {
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
srv.SetLocalBackend(lb)
@@ -449,19 +457,15 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
return nil
}
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
linkMon, err := monitor.New(logf)
if err != nil {
return nil, fmt.Errorf("monitor.New: %w", err)
}
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (_ *ipnlocal.LocalBackend, retErr error) {
if logPol != nil {
logPol.Logtail.SetLinkMonitor(linkMon)
logPol.Logtail.SetNetMon(netMon)
}
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
e, onlyNetstack, err := createEngine(logf, netMon, dialer)
if err != nil {
return nil, fmt.Errorf("createEngine: %w", err)
}
@@ -494,11 +498,13 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
}
}
if socksListener != nil || httpProxyListener != nil {
var addrs []string
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
addrs = append(addrs, httpProxyListener.Addr().String())
}
if socksListener != nil {
ss := &socks5.Server{
@@ -508,7 +514,9 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
go func() {
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
}()
addrs = append(addrs, socksListener.Addr().String())
}
tshttpproxy.SetSelfProxy(addrs...)
}
e = wgengine.NewWatchdog(e)
@@ -520,7 +528,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
return nil, fmt.Errorf("store.New: %w", err)
}
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, e, opts.LoginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, logID, store, dialer, e, opts.LoginFlags)
if err != nil {
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
}
@@ -529,7 +537,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
lb.SetLogFlusher(logPol.Logtail.StartFlush)
}
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
}
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
@@ -546,14 +554,14 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
//
// onlyNetstack is true if the user has explicitly requested that we use netstack
// for all networking.
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
func createEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack 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, onlyNetstack, err = tryEngine(logf, linkMon, dialer, name)
e, onlyNetstack, err = tryEngine(logf, netMon, dialer, name)
if err == nil {
return e, onlyNetstack, nil
}
@@ -585,11 +593,11 @@ func handleSubnetsInNetstack() bool {
var tstunNew = tstun.New
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
ListenPort: args.port,
NetMon: netMon,
Dialer: dialer,
}
onlyNetstack = name == "userspace-networking"
@@ -628,7 +636,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
return e, false, err
}
r, err := router.New(logf, dev, linkMon)
r, err := router.New(logf, dev, netMon)
if err != nil {
dev.Close()
return nil, false, fmt.Errorf("creating router: %w", err)
@@ -665,7 +673,7 @@ func newDebugMux() *http.ServeMux {
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
tsweb.VarzHandler(w, r)
varz.Handler(w, r)
clientmetric.WritePrometheusExpositionFormat(w)
}

View File

@@ -45,8 +45,10 @@ import (
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
@@ -262,13 +264,13 @@ func beWindowsSubprocess() bool {
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logid := os.Args[2]
logID := os.Args[2]
// Remove the date/time prefix; the logtail + file loggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long(), os.Args)
log.Printf("subproc mode: logid=%v", logid)
log.Printf("subproc mode: logid=%v", logID)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
@@ -290,7 +292,13 @@ func beWindowsSubprocess() bool {
}
}()
err := startIPNServer(ctx, log.Printf, logid)
netMon, err := netmon.New(log.Printf)
if err != nil {
log.Printf("Could not create netMon: %v", err)
netMon = nil
}
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, netMon)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}

View File

@@ -28,10 +28,13 @@ const (
func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
// Change cwd to to where this file lives -- that's where all inputs for
// esbuild and other build steps live.
if _, filename, _, ok := runtime.Caller(0); ok {
if err := os.Chdir(path.Dir(filename)); err != nil {
return nil, fmt.Errorf("Cannot change cwd: %w", err)
}
root, err := findRepoRoot()
if err != nil {
return nil, err
}
tsConnectDir := filepath.Join(root, "cmd", "tsconnect")
if err := os.Chdir(tsConnectDir); err != nil {
return nil, fmt.Errorf("Cannot change cwd: %w", err)
}
if err := installJSDeps(); err != nil {
return nil, fmt.Errorf("Cannot install JS deps: %w", err)
@@ -67,6 +70,25 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
}, nil
}
func findRepoRoot() (string, error) {
if *rootDir != "" {
return *rootDir, nil
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(path.Join(cwd, "go.mod")); err == nil {
return cwd, nil
}
if cwd == "/" {
return "", fmt.Errorf("Cannot find repo root")
}
cwd = path.Dir(cwd)
}
}
func commonPkgSetup(dev bool) (*esbuild.BuildOptions, error) {
buildOptions, err := commonSetup(dev)
if err != nil {

View File

@@ -38,13 +38,31 @@ class App extends Component<{}, AppState> {
if (ipnState === "NeedsMachineAuth") {
machineAuthInstructions = (
<div class="container mx-auto px-4 text-center">
An administrator needs to authorize this device.
An administrator needs to approve this device.
</div>
)
}
const lockedOut = netMap?.lockedOut
let lockedOutInstructions
if (lockedOut) {
lockedOutInstructions = (
<div class="container mx-auto px-4 text-center space-y-4">
<p>This instance of Tailscale Connect needs to be signed, due to
{" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
being enabled on this domain.
</p>
<p>
Run the following command on a device with a trusted tailnet lock key:
<pre>tailscale lock sign {netMap.self.nodeKey}</pre>
</p>
</div>
)
}
let ssh
if (ipn && ipnState === "Running" && netMap) {
if (ipn && ipnState === "Running" && netMap && !lockedOut) {
ssh = <SSH netMap={netMap} ipn={ipn} />
}
@@ -55,6 +73,7 @@ class App extends Component<{}, AppState> {
<div class="flex-grow flex flex-col justify-center overflow-hidden">
{urlDisplay}
{machineAuthInstructions}
{lockedOutInstructions}
{ssh}
</div>
</>

View File

@@ -30,7 +30,7 @@ const STATE_LABELS = {
NoState: "Initializing…",
InUseOtherUser: "In-use by another user",
NeedsLogin: "Needs login",
NeedsMachineAuth: "Needs authorization",
NeedsMachineAuth: "Needs approval",
Stopped: "Stopped",
Starting: "Starting…",
Running: "Running",

View File

@@ -60,11 +60,11 @@ function SSHSession({
function NoSSHPeers() {
return (
<div class="container mx-auto px-4 text-center">
None of your machines have
None of your machines have{" "}
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
Tailscale SSH
</a>
enabled. Give it a try!
{" "}enabled. Give it a try!
</div>
)
}

View File

@@ -63,6 +63,7 @@ declare global {
type IPNNetMap = {
self: IPNNetMapSelfNode
peers: IPNNetMapPeerNode[]
lockedOut: boolean
}
type IPNNetMapNode = {

View File

@@ -23,6 +23,7 @@ var (
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
)
func main() {

View File

@@ -46,7 +46,7 @@ import (
var ControlURL = ipn.DefaultControlURL
func main() {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Fatal("Usage: newIPN(config)")
return nil
@@ -122,8 +122,8 @@ func newIPN(jsConfig js.Value) map[string]any {
return ns.DialContextTCP(ctx, dst)
}
logid := lpc.PublicID.String()
srv := ipnserver.New(logf, logid)
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
@@ -146,7 +146,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
return map[string]any{
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Fatal(`Usage: run({
notifyState(state: int): void,
@@ -159,7 +159,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.run(args[0])
return nil
}),
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 0 {
log.Printf("Usage: login()")
return nil
@@ -167,7 +167,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.login()
return nil
}),
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 0 {
log.Printf("Usage: logout()")
return nil
@@ -175,7 +175,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.logout()
return nil
}),
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 3 {
log.Printf("Usage: ssh(hostname, userName, termConfig)")
return nil
@@ -185,7 +185,7 @@ func newIPN(jsConfig js.Value) map[string]any {
args[1].String(),
args[2])
}),
"fetch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Printf("Usage: fetch(url)")
return nil
@@ -272,6 +272,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
}
}),
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
}
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
@@ -333,10 +334,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
go jsSSHSession.Run()
return map[string]any{
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
return jsSSHSession.Close() != nil
}),
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
rows := args[0].Int()
cols := args[1].Int()
return jsSSHSession.Resize(rows, cols) != nil
@@ -425,7 +426,7 @@ func (s *jsSSHSession) Run() {
session.Stdout = termWriter{writeFn}
session.Stderr = termWriter{writeFn}
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
input := args[0].String()
_, err := stdin.Write([]byte(input))
if err != nil {
@@ -495,7 +496,7 @@ func (i *jsIPN) fetch(url string) js.Value {
return map[string]any{
"status": res.StatusCode,
"statusText": res.Status,
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
return makePromise(func() (any, error) {
defer res.Body.Close()
buf := new(bytes.Buffer)
@@ -521,8 +522,9 @@ func (w termWriter) Write(p []byte) (n int, err error) {
}
type jsNetMap struct {
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`
LockedOut bool `json:"lockedOut"`
}
type jsNetMapNode struct {
@@ -600,7 +602,7 @@ func generateHostname() string {
// f is run on a goroutine and its return value is used to resolve the promise
// (or reject it if an error is returned).
func makePromise(f func() (any, error)) js.Value {
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
resolve := args[0]
reject := args[1]
go func() {

View File

@@ -398,7 +398,7 @@ type maxMsgBuffer [maxMessageSize]byte
// bufPool holds the temporary buffers for Conn.Read & Write.
var bufPool = &sync.Pool{
New: func() interface{} {
New: func() any {
return new(maxMsgBuffer)
},
}

View File

@@ -13,6 +13,7 @@ import (
"tailscale.com/health"
"tailscale.com/logtail/backoff"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -58,15 +59,17 @@ type Auto struct {
mu sync.Mutex // mutex guards the following fields
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
inSendStatus int // number of sendStatus calls currently in progress
state State
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
liteMapUpdateCancel context.CancelFunc // cancels a lite map update, may be nil
liteMapUpdateCancels int // how many times we've canceled a lite map update
inSendStatus int // number of sendStatus calls currently in progress
state State
authCtx context.Context // context used for auth requests
mapCtx context.Context // context used for netmap requests
@@ -118,7 +121,11 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
statusFunc: opts.Status,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
return c, nil
@@ -163,28 +170,56 @@ func (c *Auto) Start() {
func (c *Auto) sendNewMapRequest() {
c.mu.Lock()
// If we're not already streaming a netmap, or if we're already stuck
// in a lite update, then tear down everything and start a new stream
// (which starts by sending a new map request)
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
// If we're not already streaming a netmap, then tear down everything
// and start a new stream (which starts by sending a new map request)
if !c.inPollNetMap || !c.loggedIn {
c.mu.Unlock()
c.cancelMapSafely()
return
}
// If we are already in process of doing a LiteMapUpdate, cancel it and
// try a new one. If this is the 10th time we have done this
// cancelation, tear down everything and start again.
const maxLiteMapUpdateAttempts = 10
if c.inLiteMapUpdate {
// Always cancel the in-flight lite map update, regardless of
// whether we cancel the streaming map request or not.
c.liteMapUpdateCancel()
c.inLiteMapUpdate = false
if c.liteMapUpdateCancels >= maxLiteMapUpdateAttempts {
// Not making progress
c.mu.Unlock()
c.cancelMapSafely()
return
}
// Increment our cancel counter and continue below to start a
// new lite update.
c.liteMapUpdateCancels++
}
// Otherwise, send a lite update that doesn't keep a
// long-running stream response.
defer c.mu.Unlock()
c.inLiteMapUpdate = true
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
c.liteMapUpdateCancel = cancel
go func() {
defer cancel()
t0 := time.Now()
err := c.direct.SendLiteMapUpdate(ctx)
d := time.Since(t0).Round(time.Millisecond)
c.mu.Lock()
c.inLiteMapUpdate = false
c.liteMapUpdateCancel = nil
if err == nil {
c.liteMapUpdateCancels = 0
}
c.mu.Unlock()
if err == nil {
c.logf("[v1] successful lite map update in %v", d)
return
@@ -192,10 +227,13 @@ func (c *Auto) sendNewMapRequest() {
if ctx.Err() == nil {
c.logf("lite map update after %v: %v", d, err)
}
// Fall back to restarting the long-polling map
// request (the old heavy way) if the lite update
// failed for any reason.
c.cancelMapSafely()
if !errors.Is(ctx.Err(), context.Canceled) {
// Fall back to restarting the long-polling map
// request (the old heavy way) if the lite update
// failed for reasons other than the context being
// canceled.
c.cancelMapSafely()
}
}()
}
@@ -206,6 +244,7 @@ func (c *Auto) cancelAuth() {
}
if !c.closed {
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
}
c.mu.Unlock()
}
@@ -216,6 +255,8 @@ func (c *Auto) cancelMapLocked() {
}
if !c.closed {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
}
}
@@ -229,6 +270,12 @@ func (c *Auto) cancelMapSafely() {
c.mu.Lock()
defer c.mu.Unlock()
// Always reset our lite map cancels counter if we're canceling
// everything, since we're about to restart with a new map update; this
// allows future calls to sendNewMapRequest to retry sending lite
// updates.
c.liteMapUpdateCancels = 0
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
if c.inPollNetMap {
@@ -360,7 +407,13 @@ func (c *Auto) authRoutine() {
c.mu.Unlock()
c.sendStatus("authRoutine-url", err, url, nil)
bo.BackOff(ctx, err)
if goal.url == url {
// The server sent us the same URL we already tried,
// backoff to avoid a busy loop.
bo.BackOff(ctx, errors.New("login URL not changing"))
} else {
bo.BackOff(ctx, nil)
}
continue
}

View File

@@ -7,10 +7,11 @@ import (
"bufio"
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
@@ -36,6 +37,7 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tsdial"
@@ -53,20 +55,20 @@ import (
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/systemd"
"tailscale.com/wgengine/monitor"
)
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
serverURL string // URL of the tailcontrol server
timeNow func() time.Time
lastPrintMap time.Time
newDecompressor func() (Decompressor, error)
keepAlive bool
logf logger.Logf
linkMon *monitor.Mon // or nil
netMon *netmon.Monitor // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
@@ -87,16 +89,15 @@ type Direct struct {
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
noiseClient *NoiseClient
persist persist.PersistView
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
tkaHead string
everEndpoints bool // whether we've ever had non-empty endpoints
lastPingURL string // last PingRequest.URL received, for dup suppression
persist persist.PersistView
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
tkaHead string
lastPingURL string // last PingRequest.URL received, for dup suppression
}
type Options struct {
@@ -113,7 +114,7 @@ type Options struct {
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor
NetMon *netmon.Monitor // optional network monitor
PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
OnControlTime func(time.Time) // optional func to notify callers of new time from control
@@ -199,6 +200,14 @@ func NewDirect(opts Options) (*Direct, error) {
opts.Logf = log.Printf
}
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, opts.NetMon),
Logf: opts.Logf,
NetMon: opts.NetMon,
}
httpc := opts.HTTPTestClient
if httpc == nil && runtime.GOOS == "js" {
// In js/wasm, net/http.Transport (as of Go 1.18) will
@@ -208,11 +217,6 @@ func NewDirect(opts Options) (*Direct, error) {
httpc = http.DefaultClient
}
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
@@ -240,7 +244,7 @@ func NewDirect(opts Options) (*Direct, error) {
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
linkMon: opts.LinkMonitor,
netMon: opts.NetMon,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
@@ -248,6 +252,7 @@ func NewDirect(opts Options) (*Direct, error) {
onControlTime: opts.OnControlTime,
c2nHandler: opts.C2NHandler,
dialer: opts.Dialer,
dnsCache: dnsCache,
dialPlan: opts.DialPlan,
}
if opts.Hostinfo == nil {
@@ -424,7 +429,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
tryingNewKey := c.tryingNewKey
serverKey := c.serverKey
serverNoiseKey := c.serverNoiseKey
authKey := c.authKey
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
@@ -510,6 +515,22 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
c.logf("Failed re-signing node-key signature: %v", err)
}
} else if isWrapped {
// We were given a wrapped pre-auth key, which means that in addition
// to being a regular pre-auth key there was a suffix with information to
// generate a tailnet-lock signature.
nk, err := tryingNewKey.Public().MarshalBinary()
if err != nil {
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
}
sig := &tka.NodeKeySignature{
SigKind: tka.SigRotation,
Pubkey: nk,
Nested: wrappedSig,
}
sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
nodeKeySignature = sig.Serialize()
}
if backendLogID == "" {
@@ -735,9 +756,6 @@ func (c *Direct) newEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
}
c.logf("[v2] client.newEndpoints(%v)", epStrs)
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
c.everEndpoints = true
}
return true // changed
}
@@ -750,8 +768,6 @@ func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
return c.newEndpoints(endpoints)
}
func inTest() bool { return flag.Lookup("test.v") != nil }
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
@@ -806,7 +822,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
epStrs = append(epStrs, ep.Addr.String())
epTypes = append(epTypes, ep.Type)
}
everEndpoints := c.everEndpoints
c.mu.Unlock()
machinePrivKey, err := c.getMachinePrivKey()
@@ -847,19 +862,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
OmitPeers: cb == nil,
TKAHead: c.tkaHead,
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyper-specific about the
// ordering of things. The e2e tests need love.
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
// Previously we'd set ReadOnly to true if we didn't have any endpoints
// yet as we expected to learn them in a half second and restart the full
// streaming map poll, however as we are trying to reduce the number of
// times we restart the full streaming map poll we now just set ReadOnly
// false when we're doing a full streaming map poll.
//
// TODO(maisem/bradfitz): really ReadOnly should be set to true if for
// all streams and we should only do writes via lite map updates.
// However that requires an audit and a bunch of testing to make sure we
// don't break anything.
ReadOnly: readOnly && !allowStream,
}
var extraDebugFlags []string
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
if hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
@@ -1495,7 +1512,16 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
return nil, err
}
c.logf("creating new noise client")
nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
nc, err := NewNoiseClient(NoiseOpts{
PrivKey: k,
ServerPubKey: serverNoiseKey,
ServerURL: c.serverURL,
Dialer: c.dialer,
DNSCache: c.dnsCache,
Logf: c.logf,
NetMon: c.netMon,
DialPlan: dp,
})
if err != nil {
return nil, err
}
@@ -1713,6 +1739,43 @@ func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
res.Body.Close()
}
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
// In all cases the authkey is returned, sans wrapping information if any.
//
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
// and private key.
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
authKey, suffix, found := strings.Cut(key, "--TL")
if !found {
return key, false, nil, nil
}
sigBytes, privBytes, found := strings.Cut(suffix, "-")
if !found {
logf("decoding wrapped auth-key: did not find delimiter")
return key, false, nil, nil
}
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
if err != nil {
logf("decoding wrapped auth-key: signature decode: %v", err)
return key, false, nil, nil
}
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
if err != nil {
logf("decoding wrapped auth-key: priv decode: %v", err)
return key, false, nil, nil
}
sig = new(tka.NodeKeySignature)
if err := sig.Unserialize([]byte(rawSig)); err != nil {
logf("decoding wrapped auth-key: signature: %v", err)
return key, false, nil, nil
}
priv = ed25519.PrivateKey(rawPriv)
return authKey, true, sig, priv
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")

View File

@@ -4,6 +4,7 @@
package controlclient
import (
"crypto/ed25519"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -142,3 +143,42 @@ func TestTsmpPing(t *testing.T) {
t.Fatal(err)
}
}
func TestDecodeWrappedAuthkey(t *testing.T) {
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
}
if isWrapped {
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
}
if sig != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
}
if priv != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
}
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
}
if !isWrapped {
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
}
if sig == nil {
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
}
sigHash := sig.SigHash()
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
t.Error("signature failed to verify")
}
// Make sure the private is correct by using it.
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
t.Error("failed to use priv")
}
}

View File

@@ -13,6 +13,7 @@ import (
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
@@ -21,12 +22,11 @@ import (
)
func TestUndeltaPeers(t *testing.T) {
defer func(old func() time.Time) { clockNow = old }(clockNow)
var curTime time.Time
clockNow = func() time.Time {
tstest.Replace(t, &clockNow, func() time.Time {
return curTime
}
})
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.Online = &v

View File

@@ -19,9 +19,12 @@ import (
"golang.org/x/net/http2"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
@@ -156,6 +159,7 @@ type NoiseClient struct {
sfDial singleflight.Group[struct{}, *noiseConn]
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
privKey key.MachinePrivate
serverPubKey key.MachinePublic
host string // the host part of serverURL
@@ -167,6 +171,9 @@ type NoiseClient struct {
// be nil.
dialPlan func() *tailcfg.ControlDialPlan
logf logger.Logf
netMon *netmon.Monitor
// mu only protects the following variables.
mu sync.Mutex
last *noiseConn // or nil
@@ -174,12 +181,39 @@ type NoiseClient struct {
connPool map[int]*noiseConn // active connections not yet closed; see noiseConn.Close
}
// NoiseOpts contains options for the NewNoiseClient function. All fields are
// required unless otherwise specified.
type NoiseOpts struct {
// PrivKey is this node's private key.
PrivKey key.MachinePrivate
// ServerPubKey is the public key of the server.
ServerPubKey key.MachinePublic
// ServerURL is the URL of the server to connect to.
ServerURL string
// Dialer's SystemDial function is used to connect to the server.
Dialer *tsdial.Dialer
// DNSCache is the caching Resolver to use to connect to the server.
//
// This field can be nil.
DNSCache *dnscache.Resolver
// Logf is the log function to use. This field can be nil.
Logf logger.Logf
// NetMon is the network monitor that, if set, will be used to get the
// network interface state. This field can be nil; if so, the current
// state will be looked up dynamically.
NetMon *netmon.Monitor
// DialPlan, if set, is a function that should return an explicit plan
// on how to connect to the server.
DialPlan func() *tailcfg.ControlDialPlan
}
// NewNoiseClient returns a new noiseClient for the provided server and machine key.
// serverURL is of the form https://<host>:<port> (no trailing slash).
//
// netMon may be nil, if non-nil it's used to do faster interface lookups.
// dialPlan may be nil
func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) {
u, err := url.Parse(serverURL)
func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
u, err := url.Parse(opts.ServerURL)
if err != nil {
return nil, err
}
@@ -199,14 +233,18 @@ func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic,
httpPort = "80"
httpsPort = "443"
}
np := &NoiseClient{
serverPubKey: serverPubKey,
privKey: privKey,
serverPubKey: opts.ServerPubKey,
privKey: opts.PrivKey,
host: u.Hostname(),
httpPort: httpPort,
httpsPort: httpsPort,
dialer: dialer,
dialPlan: dialPlan,
dialer: opts.Dialer,
dnsCache: opts.DNSCache,
dialPlan: opts.DialPlan,
logf: opts.Logf,
netMon: opts.NetMon,
}
// Create the HTTP/2 Transport using a net/http.Transport
@@ -365,7 +403,10 @@ func (nc *NoiseClient) dial() (*noiseConn, error) {
ControlKey: nc.serverPubKey,
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
DNSCache: nc.dnsCache,
DialPlan: dialPlan,
Logf: nc.logf,
NetMon: nc.netMon,
}).Dial(ctx)
if err != nil {
return nil, err

View File

@@ -74,7 +74,12 @@ func (tt noiseClientTest) run(t *testing.T) {
defer hs.Close()
dialer := new(tsdial.Dialer)
nc, err := NewNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil)
nc, err := NewNoiseClient(NoiseOpts{
PrivKey: clientPrivate,
ServerPubKey: serverPrivate.Public(),
ServerURL: hs.URL,
Dialer: dialer,
})
if err != nil {
t.Fatal(err)
}

View File

@@ -41,6 +41,7 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
@@ -272,6 +273,8 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
// we'll speak Noise.
@@ -371,6 +374,22 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*Cli
}, nil
}
// resolver returns a.DNSCache if non-nil or a new *dnscache.Resolver
// otherwise.
func (a *Dialer) resolver() *dnscache.Resolver {
if a.DNSCache != nil {
return a.DNSCache
}
return &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
UseLastGood: true,
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
}
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr
// is valid, then no DNS is used and the connection will be made to the
// provided address.
@@ -385,13 +404,11 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
dns = &dnscache.Resolver{
SingleHostStaticResult: []netip.Addr{addr},
SingleHost: u.Hostname(),
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
} else {
dns = &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
}
dns = a.resolver()
}
var dialer dnscache.DialContextFunc

View File

@@ -9,6 +9,7 @@ import (
"time"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -66,10 +67,17 @@ type Dialer struct {
// If not specified, this defaults to net.Dialer.DialContext.
Dialer dnscache.DialContextFunc
// DNSCache is the caching Resolver used by this Dialer.
//
// If not specified, a new Resolver is created per attempt.
DNSCache *dnscache.Resolver
// Logf, if set, is a logging function to use; if unset, logs are
// dropped.
Logf logger.Logf
NetMon *netmon.Monitor
// DialPlan, if set, contains instructions from the control server on
// how to connect to it. If present, we will try the methods in this
// plan before falling back to DNS.

61
derp/README.md Normal file
View File

@@ -0,0 +1,61 @@
# DERP
This directory (and subdirectories) contain the DERP code. The server itself is
in `../cmd/derper`.
DERP is a packet relay system (client and servers) where peers are addressed
using WireGuard public keys instead of IP addresses.
It relays two types of packets:
* "Disco" discovery messages (see `../disco`) as the a side channel during [NAT
traversal](https://tailscale.com/blog/how-nat-traversal-works/).
* Encrypted WireGuard packets as the fallback of last resort when UDP is blocked
or NAT traversal fails.
## DERP Map
Each client receives a "[DERP
Map](https://pkg.go.dev/tailscale.com/tailcfg#DERPMap)" from the coordination
server describing the DERP servers the client should try to use.
The client picks its home "DERP home" based on latency. This is done to keep
costs low by avoid using cloud load balancers (pricey) or anycast, which would
necessarily require server-side routing between DERP regions.
Clients pick their DERP home and report it to the coordination server which
shares it to all the peers in the tailnet. When a peer wants to send a packet
and it doesn't already have a WireGuard session open, it sends disco messages
(some direct, and some over DERP), trying to do the NAT traversal. The client
will make connections to multiple DERP regions as needed. Only the DERP home
region connection needs to be alive forever.
## DERP Regions
Tailscale runs 1 or more DERP nodes (instances of `cmd/derper`) in various
geographic regions to make sure users have low latency to their DERP home.
Regions generally have multiple nodes per region "meshed" (routing to each
other) together for redundancy: it allows for cloud failures or upgrades without
kicking users out to a higher latency region. Instead, clients will reconnect to
the next node in the region. Each node in the region is required to to be meshed
with every other node in the region and forward packets to the other nodes in
the region. Packets are forwarded only one hop within the region. There is no
routing between regions. The assumption is that the mesh TCP connections are
over a VPC that's very fast, low latency, and not charged per byte. The
coordination server assigns the list of nodes in a region as a function of the
tailnet, so all nodes within a tailnet should generally be on the same node and
not require forwarding. Only after a failure do clients of a particular tailnet
get split between nodes in a region and require inter-node forwarding. But over
time it balances back out. There's also an admin-only DERP frame type to force
close the TCP connection of a particular client to force them to reconnect to
their primary if the operator wants to force things to balance out sooner.
(Using the `(*derphttp.Client).ClosePeer` method, as used by Tailscale's
internal rarely-used `cmd/derpprune` maintenance tool)
We generally run a minimum of three nodes in a region not for quorum reasons
(there's no voting) but just because two is too uncomfortably few for cascading
failure reasons: if you're running two nodes at 51% load (CPU, memory, etc) and
then one fails, that makes the second one fail. With three or more nodes, you
can run each node a bit hotter.

View File

@@ -77,8 +77,11 @@ const (
// a previous sender is no longer connected. That is, if A
// sent to B, and then if A disconnects, the server sends
// framePeerGone to B so B can forget that a reverse path
// exists on that connection to get back to A.
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone
// exists on that connection to get back to A. It is also sent
// if A tries to send a CallMeMaybe to B and the server has no
// record of B (which currently would only happen if there was
// a bug).
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
// framePeerPresent is like framePeerGone, but for other
// members of the DERP region when they're meshed up together.
@@ -116,6 +119,15 @@ const (
frameRestarting = frameType(0x15)
)
// PeerGoneReasonType is a one byte reason code explaining why a
// server does not have a path to the requested destination.
type PeerGoneReasonType byte
const (
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
)
var bin = binary.BigEndian
func writeUint32(bw *bufio.Writer, v uint32) error {

View File

@@ -348,9 +348,12 @@ type ReceivedPacket struct {
func (ReceivedPacket) msg() {}
// PeerGoneMessage is a ReceivedMessage that indicates that the client
// identified by the underlying public key had previously sent you a
// packet but has now disconnected from the server.
type PeerGoneMessage key.NodePublic
// identified by the underlying public key is not connected to this
// server.
type PeerGoneMessage struct {
Peer key.NodePublic
Reason PeerGoneReasonType
}
func (PeerGoneMessage) msg() {}
@@ -524,7 +527,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.logf("[unexpected] dropping short peerGone frame from DERP server")
continue
}
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
// Backward compatibility for the older peerGone without reason byte
reason := PeerGoneReasonDisconnected
if n > keyLen {
reason = PeerGoneReasonType(b[keyLen])
}
pg := PeerGoneMessage{
Peer: key.NodePublicFromRaw32(mem.B(b[:keyLen])),
Reason: reason,
}
return pg, nil
case framePeerPresent:

View File

@@ -34,12 +34,12 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
@@ -122,7 +122,8 @@ type Server struct {
_ align64
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneFrames expvar.Int // number of peer gone frames sent
peerGoneDisconnectedFrames expvar.Int // number of peer disconnected frames sent
peerGoneNotHereFrames expvar.Int // number of peer not here frames sent
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
@@ -279,6 +280,7 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
// public key gets more than one PacketForwarder registered for it.
type PacketForwarder interface {
ForwardPacket(src, dst key.NodePublic, payload []byte) error
String() string
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
@@ -323,7 +325,8 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
s.packetsDroppedReasonCounters = []*expvar.Int{
s.packetsDroppedReason.Get("unknown_dest"),
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
s.packetsDroppedReason.Get("gone"),
s.packetsDroppedReason.Get("gone_disconnected"),
s.packetsDroppedReason.Get("gone_not_here"),
s.packetsDroppedReason.Get("queue_head"),
s.packetsDroppedReason.Get("queue_tail"),
s.packetsDroppedReason.Get("write_error"),
@@ -495,6 +498,7 @@ func (s *Server) registerClient(c *sclient) {
switch set := set.(type) {
case nil:
s.clients[c.key] = singleClient{c}
c.debug("register single client")
case singleClient:
s.dupClientKeys.Add(1)
s.dupClientConns.Add(2) // both old and new count
@@ -510,6 +514,7 @@ func (s *Server) registerClient(c *sclient) {
},
sendHistory: []*sclient{old},
}
c.debug("register duplicate client")
case *dupClientSet:
s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter
@@ -517,6 +522,7 @@ func (s *Server) registerClient(c *sclient) {
set.set[c] = true
set.last = c
set.sendHistory = append(set.sendHistory, c)
c.debug("register another duplicate client")
}
if _, ok := s.clientsMesh[c.key]; !ok {
@@ -549,7 +555,7 @@ func (s *Server) unregisterClient(c *sclient) {
case nil:
c.logf("[unexpected]; clients map is empty")
case singleClient:
c.logf("removing connection")
c.logf("removed connection")
delete(s.clients, c.key)
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
delete(s.clientsMesh, c.key)
@@ -557,6 +563,7 @@ func (s *Server) unregisterClient(c *sclient) {
}
s.broadcastPeerStateChangeLocked(c.key, false)
case *dupClientSet:
c.debug("removed duplicate client")
if set.removeClient(c) {
s.dupClientConns.Add(-1)
} else {
@@ -610,13 +617,26 @@ func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
}
set.ForeachClient(func(peer *sclient) {
if peer.connNum == connNum {
go peer.requestPeerGoneWrite(key)
go peer.requestPeerGoneWrite(key, PeerGoneReasonDisconnected)
}
})
}
delete(s.sentTo, key)
}
// requestPeerGoneWriteLimited sends a request to write a "peer gone"
// frame, but only in reply to a disco packet, and only if we haven't
// sent one recently.
func (c *sclient) requestPeerGoneWriteLimited(peer key.NodePublic, contents []byte, reason PeerGoneReasonType) {
if disco.LooksLikeDiscoWrapper(contents) != true {
return
}
if c.peerGoneLim.Allow() {
go c.requestPeerGoneWrite(peer, reason)
}
}
func (s *Server) addWatcher(c *sclient) {
if !c.canMesh {
panic("invariant: addWatcher called without permissions")
@@ -673,7 +693,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
nc: nc,
br: br,
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
@@ -681,8 +701,9 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan key.NodePublic),
peerGone: make(chan peerGoneMsg),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
}
if c.canMesh {
@@ -690,6 +711,9 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
}
if clientInfo != nil {
c.info = *clientInfo
if envknob.Bool("DERP_PROBER_DEBUG_LOGS") && clientInfo.IsProber {
c.debugLogging = true
}
}
s.registerClient(c)
@@ -726,6 +750,7 @@ func (c *sclient) run(ctx context.Context) error {
for {
ft, fl, err := readFrameHeader(c.br)
c.debug("read frame type %d len %d err %v", ft, fl, err)
if err != nil {
if errors.Is(err, io.EOF) {
c.logf("read EOF")
@@ -735,7 +760,7 @@ func (c *sclient) run(ctx context.Context) error {
c.logf("closing; server closed")
return nil
}
return fmt.Errorf("client %x: readFrameHeader: %w", c.key, err)
return fmt.Errorf("client %s: readFrameHeader: %w", c.key.ShortString(), err)
}
c.s.noteClientActivity(c)
switch ft {
@@ -878,11 +903,15 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDestOnFwd
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, srcKey, dstKey, reason)
return nil
}
dst.debug("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
return c.sendPkt(dst, pkt{
bs: contents,
enqueuedAt: time.Now(),
@@ -930,7 +959,9 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
if dst == nil {
if fwd != nil {
s.packetsForwardedOut.Add(1)
if err := fwd.ForwardPacket(c.key, dstKey, contents); err != nil {
err := fwd.ForwardPacket(c.key, dstKey, contents)
c.debug("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
if err != nil {
// TODO:
return nil
}
@@ -939,10 +970,14 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDest
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, c.key, dstKey, reason)
c.debug("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
return nil
}
c.debug("SendPacket for %s, sending directly", dstKey.ShortString())
p := pkt{
bs: contents,
@@ -952,6 +987,12 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
return c.sendPkt(dst, p)
}
func (c *sclient) debug(format string, v ...any) {
if c.debugLogging {
c.logf(format, v...)
}
}
// dropReason is why we dropped a DERP frame.
type dropReason int
@@ -960,7 +1001,7 @@ type dropReason int
const (
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
dropReasonGone // destination tailscaled disconnected before we could send
dropReasonGoneDisconnected // destination tailscaled disconnected before we could send
dropReasonQueueHead // destination queue is full, dropped packet at queue head
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
dropReasonWriteError // OS write() failed
@@ -1002,12 +1043,14 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
for attempt := 0; attempt < 3; attempt++ {
select {
case <-dst.done:
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
s.recordDrop(p.bs, c.key, dstKey, dropReasonGoneDisconnected)
dst.debug("sendPkt attempt %d dropped, dst gone", attempt)
return nil
default:
}
select {
case sendQueue <- p:
dst.debug("sendPkt attempt %d enqueued", attempt)
return nil
default:
}
@@ -1023,16 +1066,20 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
// contended queue with racing writers. Give up and tail-drop in
// this case to keep reader unblocked.
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
dst.debug("sendPkt attempt %d dropped, queue full")
return nil
}
// requestPeerGoneWrite sends a request to write a "peer gone" frame
// that the provided peer has disconnected. It blocks until either the
// with an explanation of why it is gone. It blocks until either the
// write request is scheduled, or the client has closed.
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReasonType) {
select {
case c.peerGone <- peer:
case c.peerGone <- peerGoneMsg{
peer: peer,
reason: reason,
}:
case <-c.done:
}
}
@@ -1246,22 +1293,19 @@ type sclient struct {
key key.NodePublic
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan peerGoneMsg // write request that a peer is not at this server (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
// replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by
// taking over ownership of a key.
replaceLimiter *rate.Limiter
debugLogging bool
// Owned by run, not thread-safe.
br *bufio.Reader
@@ -1278,6 +1322,11 @@ type sclient struct {
// the client for them to update their map of who's connected
// to this node.
peerStateChange []peerConnState
// peerGoneLimiter limits how often the server will inform a
// client that it's trying to establish a direct connection
// through us with a peer we have no record of.
peerGoneLim *rate.Limiter
}
// peerConnState represents whether a peer is connected to the server
@@ -1301,6 +1350,12 @@ type pkt struct {
bs []byte
}
// peerGoneMsg is a request to write a peerGone frame to an sclient
type peerGoneMsg struct {
peer key.NodePublic
reason PeerGoneReasonType
}
func (c *sclient) setPreferred(v bool) {
if c.preferred == v {
return
@@ -1355,9 +1410,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
for {
select {
case pkt := <-c.sendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
case pkt := <-c.discoSendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
default:
return
}
@@ -1378,8 +1433,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
continue
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
@@ -1410,8 +1465,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
continue
@@ -1452,13 +1507,22 @@ func (c *sclient) sendPong(data [8]byte) error {
}
// sendPeerGone sends a peerGone frame, without flushing.
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
c.s.peerGoneFrames.Add(1)
func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) error {
switch reason {
case PeerGoneReasonDisconnected:
c.s.peerGoneDisconnectedFrames.Add(1)
case PeerGoneReasonNotHere:
c.s.peerGoneNotHereFrames.Add(1)
}
c.setWriteDeadline()
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
data := make([]byte, 0, keyLen+1)
data = peer.AppendTo(data)
data = append(data, byte(reason))
if err := writeFrameHeader(c.bw.bw(), framePeerGone, uint32(len(data))); err != nil {
return err
}
_, err := c.bw.Write(peer.AppendTo(nil))
_, err := c.bw.Write(data)
return err
}
@@ -1489,7 +1553,7 @@ func (c *sclient) sendMeshUpdates() error {
if pcs.present {
err = c.sendPeerPresent(pcs.peer)
} else {
err = c.sendPeerGone(pcs.peer)
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
}
if err != nil {
// Shouldn't happen, though, as we're writing
@@ -1529,6 +1593,7 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
c.s.packetsSent.Add(1)
c.s.bytesSent.Add(int64(len(contents)))
}
c.debug("sendPacket from %s: %v", srcKey.ShortString(), err)
}()
c.setWriteDeadline()
@@ -1689,6 +1754,10 @@ func (f *multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte)
return f.fwd.Load().ForwardPacket(src, dst, payload)
}
func (f *multiForwarder) String() string {
return fmt.Sprintf("<MultiForwarder fwd=%s total=%d>", f.fwd.Load(), len(f.all))
}
func (s *Server) expVarFunc(f func() any) expvar.Func {
return expvar.Func(func() any {
s.mu.Lock()
@@ -1725,7 +1794,8 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("home_moves_out", &s.homeMovesOut)
m.Set("got_ping", &s.gotPing)
m.Set("sent_pong", &s.sentPong)
m.Set("peer_gone_frames", &s.peerGoneFrames)
m.Set("peer_gone_disconnected_frames", &s.peerGoneDisconnectedFrames)
m.Set("peer_gone_not_here_frames", &s.peerGoneNotHereFrames)
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
m.Set("multiforwarder_created", &s.multiForwarderCreated)

View File

@@ -25,6 +25,7 @@ import (
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/disco"
"tailscale.com/net/memnet"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -105,7 +106,8 @@ func TestSendRecv(t *testing.T) {
t.Logf("Connected client %d.", i)
}
var peerGoneCount expvar.Int
var peerGoneCountDisconnected expvar.Int
var peerGoneCountNotHere expvar.Int
t.Logf("Starting read loops")
for i := 0; i < numClients; i++ {
@@ -121,7 +123,14 @@ func TestSendRecv(t *testing.T) {
t.Errorf("unexpected message type %T", m)
continue
case PeerGoneMessage:
peerGoneCount.Add(1)
switch m.Reason {
case PeerGoneReasonDisconnected:
peerGoneCountDisconnected.Add(1)
case PeerGoneReasonNotHere:
peerGoneCountNotHere.Add(1)
default:
t.Errorf("unexpected PeerGone reason %v", m.Reason)
}
case ReceivedPacket:
if m.Source.IsZero() {
t.Errorf("zero Source address in ReceivedPacket")
@@ -171,7 +180,19 @@ func TestSendRecv(t *testing.T) {
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCount.Value(); got == want {
if got = peerGoneCountDisconnected.Value(); got == want {
return
}
}
t.Errorf("peer gone count = %v; want %v", got, want)
}
wantUnknownPeers := func(want int64) {
t.Helper()
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCountNotHere.Value(); got == want {
return
}
}
@@ -194,6 +215,30 @@ func TestSendRecv(t *testing.T) {
recvNothing(0)
recvNothing(1)
// Send messages to a non-existent node
neKey := key.NewNode().Public()
msg4 := []byte("not a CallMeMaybe->unknown destination\n")
if err := clients[1].Send(neKey, msg4); err != nil {
t.Fatal(err)
}
wantUnknownPeers(0)
callMe := neKey.AppendTo([]byte(disco.Magic))
callMeHeader := make([]byte, disco.NonceLen)
callMe = append(callMe, callMeHeader...)
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
wantUnknownPeers(1)
// PeerGoneNotHere is rate-limited to 3 times a second
for i := 0; i < 5; i++ {
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
}
wantUnknownPeers(3)
wantActive(3, 0)
clients[0].NotePreferred(true)
wantActive(3, 1)
@@ -595,10 +640,14 @@ func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
}
switch m := m.(type) {
case PeerGoneMessage:
got := key.NodePublic(m)
got := key.NodePublic(m.Peer)
if peer != got {
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
}
reason := m.Reason
if reason != PeerGoneReasonDisconnected {
t.Errorf("got gone message for reason %v; wanted %v", reason, PeerGoneReasonDisconnected)
}
default:
t.Fatalf("unexpected message type %T", m)
}
@@ -660,6 +709,9 @@ type testFwd int
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
panic("not called in tests")
}
func (testFwd) String() string {
panic("not called in tests")
}
func pubAll(b byte) (ret key.NodePublic) {
var bs [32]byte
@@ -787,6 +839,7 @@ type channelFwd struct {
c chan []byte
}
func (f channelFwd) String() string { return "" }
func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error {
f.c <- packet
return nil

View File

@@ -31,7 +31,9 @@ import (
"tailscale.com/derp"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
@@ -54,6 +56,7 @@ type Client struct {
privateKey key.NodePrivate
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// Either url or getRegion is non-nil:
@@ -81,13 +84,19 @@ type Client struct {
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
}
func (c *Client) String() string {
return fmt.Sprintf("<derphttp_client.Client %s url=%s>", c.serverPubKey.ShortString(), c.url)
}
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
// To trigger a connection, use Connect.
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client {
ctx, cancel := context.WithCancel(context.Background())
c := &Client{
privateKey: privateKey,
logf: logf,
netMon: netMon,
getRegion: getRegion,
ctx: ctx,
cancelCtx: cancel,
@@ -169,6 +178,10 @@ func urlPort(u *url.URL) string {
return ""
}
// debugDERPUseHTTP tells clients to connect to DERP via HTTP on port
// 3340 instead of HTTPS on 443.
var debugUseDERPHTTP = envknob.RegisterBool("TS_DEBUG_USE_DERP_HTTP")
func (c *Client) targetString(reg *tailcfg.DERPRegion) string {
if c.url != nil {
return c.url.String()
@@ -180,6 +193,10 @@ func (c *Client) useHTTPS() bool {
if c.url != nil && c.url.Scheme == "http" {
return false
}
if debugUseDERPHTTP() {
return false
}
return true
}
@@ -195,7 +212,11 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
if c.url != nil {
return c.url.String()
}
return fmt.Sprintf("https://%s/derp", node.HostName)
proto := "https"
if debugUseDERPHTTP() {
proto = "http"
}
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
}
// AddressFamilySelector decides whether IPv6 is preferred for
@@ -320,7 +341,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
}
c.serverPubKey = derpClient.ServerPublicKey()
c.client = derpClient
c.netConn = tcpConn
c.netConn = conn
c.connGen++
return c.client, c.connGen, nil
case c.url != nil:
@@ -475,7 +496,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
}
hostOrIP := host
dialer := netns.NewDialer(c.logf)
dialer := netns.NewDialer(c.logf, c.netMon)
if c.DNSCache != nil {
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
@@ -570,7 +591,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr)
}
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
@@ -615,6 +636,8 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient, c.logf)
nwait := 0
startDial := func(dstPrimary, proto string) {
nwait++

View File

@@ -128,7 +128,17 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
case derp.PeerPresentMessage:
updatePeer(key.NodePublic(m), true)
case derp.PeerGoneMessage:
updatePeer(key.NodePublic(m), false)
switch m.Reason {
case derp.PeerGoneReasonDisconnected:
// Normal case, log nothing
case derp.PeerGoneReasonNotHere:
logf("Recv: peer %s not connected to %s",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString())
default:
logf("Recv: peer %s not at server %s for unknown reason %v",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
}
updatePeer(key.NodePublic(m.Peer), false)
default:
continue
}

View File

@@ -13,16 +13,16 @@ func _() {
var x [1]struct{}
_ = x[dropReasonUnknownDest-0]
_ = x[dropReasonUnknownDestOnFwd-1]
_ = x[dropReasonGone-2]
_ = x[dropReasonGoneDisconnected-2]
_ = x[dropReasonQueueHead-3]
_ = x[dropReasonQueueTail-4]
_ = x[dropReasonWriteError-5]
_ = x[dropReasonDupClient-6]
}
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClient"
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClient"
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68}
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80}
func (i dropReason) String() string {
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {

View File

@@ -0,0 +1,56 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package permissions provides a doctor.Check that prints the process
// permissions for the running process.
package permissions
import (
"context"
"fmt"
"os/user"
"strings"
"golang.org/x/exp/constraints"
"tailscale.com/types/logger"
)
// Check implements the doctor.Check interface.
type Check struct{}
func (Check) Name() string {
return "permissions"
}
func (Check) Run(_ context.Context, logf logger.Logf) error {
return permissionsImpl(logf)
}
func formatUserID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if uu, err := user.LookupId(idStr); err != nil {
return idStr + "(<unknown>)"
} else {
return fmt.Sprintf("%s(%q)", idStr, uu.Username)
}
}
func formatGroupID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if g, err := user.LookupGroupId(idStr); err != nil {
return idStr + "(<unknown>)"
} else {
return fmt.Sprintf("%s(%q)", idStr, g.Name)
}
}
func formatGroups[T constraints.Integer](groups []T) string {
var buf strings.Builder
for i, group := range groups {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(formatGroupID(group))
}
return buf.String()
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || freebsd || openbsd
package permissions
import (
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
groups, _ := unix.Getgroups()
logf("uid=%s euid=%s gid=%s egid=%s groups=%s",
formatUserID(unix.Getuid()),
formatUserID(unix.Geteuid()),
formatGroupID(unix.Getgid()),
formatGroupID(unix.Getegid()),
formatGroups(groups),
)
return nil
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package permissions
import (
"fmt"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
// NOTE: getresuid and getresgid never fail unless passed an
// invalid address.
var ruid, euid, suid uint64
unix.Syscall(unix.SYS_GETRESUID,
uintptr(unsafe.Pointer(&ruid)),
uintptr(unsafe.Pointer(&euid)),
uintptr(unsafe.Pointer(&suid)),
)
var rgid, egid, sgid uint64
unix.Syscall(unix.SYS_GETRESGID,
uintptr(unsafe.Pointer(&rgid)),
uintptr(unsafe.Pointer(&egid)),
uintptr(unsafe.Pointer(&sgid)),
)
groups, _ := unix.Getgroups()
var buf strings.Builder
fmt.Fprintf(&buf, "ruid=%s euid=%s suid=%s rgid=%s egid=%s sgid=%s groups=%s",
formatUserID(ruid), formatUserID(euid), formatUserID(suid),
formatGroupID(rgid), formatGroupID(egid), formatGroupID(sgid),
formatGroups(groups),
)
// Get process capabilities
var (
capHeader = unix.CapUserHeader{
Version: unix.LINUX_CAPABILITY_VERSION_3,
Pid: 0, // 0 means 'ourselves'
}
capData unix.CapUserData
)
if err := unix.Capget(&capHeader, &capData); err != nil {
fmt.Fprintf(&buf, " caperr=%v", err)
} else {
fmt.Fprintf(&buf, " cap_effective=%08x cap_permitted=%08x cap_inheritable=%08x",
capData.Effective, capData.Permitted, capData.Inheritable,
)
}
logf("%s", buf.String())
return nil
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(linux || darwin || freebsd || openbsd)
package permissions
import (
"runtime"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
logf("unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
return nil
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package permissions
import "testing"
func TestPermissionsImpl(t *testing.T) {
if err := permissionsImpl(t.Logf); err != nil {
t.Error(err)
}
}

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