Compare commits

...

47 Commits

Author SHA1 Message Date
Denton Gentry
0f1273ea69 CI: use BuildJet & large Windows runners
1. Use buildjet for the longer Linux CI workflows.
2. Continue with `ubuntu-latest` for shorter Linux CI workflows.
3. Use a large GitHub-hosted Windows runner.
   https://docs.github.com/en/actions/using-github-hosted-runners/using-larger-runners
4. Make CIFuzz run on merge to main or release branch, not
   in the pull request

Two runs each of the original workflow files and the updated
workflows in this PR. One run of the GitHub Large Ubuntu runners
is included; these are clearly between the regular GitHub and BuildJet
runners in performance.
                              GitHub  GitHub  BuildJet BuildJet  GHLarge
codeql-analysis.yml           4m 30s  cached   2m 56s   2m 59s   4m  0s
cross-darwin.yml              3m 10s  3m 19s   1m 33s   1m 30s   2m 43s
cross-freebsd.yml             3m 33s  3m 10s   1m 28s   1m 22s   2m 15s
cross-openbsd.yml             3m  4s  2m 36s   1m 29s   1m 22s   2m  3s
cross-wasm.yml                1m 59s  2m  2s   1m 12s   1m 16s   1m 46s
cross-windows.yml             2m 45s  3m  0s   1m 44s   1m 25s   2m  6s
linux32.yml                   4m 27s  4m  0s   1m 55s   2m  8s   2m 51s
linux-race.yml                3m 54s  4m  7s   2m 22s   2m 12s   3m 14s
linux.yml                     4m 23s  4m 39s   2m 37s   2m 15s   3m 38s
static-analysis.yml
 /vet                         1m 41s  2m 22s      52s      56s   1m 12s
 /staticcheck(linux, amd64)   2m 47s  2m 38s   1m  7s   1m 10s   1m 52s
 /staticcheck(windows, amd64) 2m  5s  2m  4s   1m  6s   1m  8s   1m 33s
 /staticcheck(darwin, amd64)  2m 14s  2m 20s   1m 10s   1m 10s   1m 50s
 /staticcheck(windows, 386)   2m 36s  1m 58s   1m 23s   1m  8s   1m 39s
vm.yml                        1m 30s  1m 32s   2m 31s   2m 23s   N/A

A few very short workflows are being left on GitHub-hosted runners, like
licenses and gofmt. These benefit from the quicker dispatch to GitHub
hosted runners.

--------

For Windows and the windows.yml test run:
- the regular `windows-latest` runner takes about 6 minutes 20 seconds
- there is enough variability run to run that we get the same ~4 minute
  run with:
    - a GitHub-hosted large runner
    - a self-hosted Windows Server 2022 in an AWS t3.xlarge
    - a self-hosted Windows Server 2022 in an AWS c6i.xlarge
Since there is not a gain from operating our own runner, we'll pay
GitHub to operate a Windows large runner.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-09-24 17:22:04 -07:00
Brad Fitzpatrick
fb4e23506f control/controlclient: stop restarting map polls on health change
At some point we started restarting map polls on health change, but we
don't remember why. Maybe it was a desperate workaround for something.
I'm not sure it ever worked.

Rather than have a haunted graveyard, remove it.

In its place, though, and somewhat as a safety backup, send those
updates over the HTTP/2 noise channel if we have one open. Then if
there was a reason that a map poll restart would help we could do it
server-side. But mostly we can gather error stats and show
machine-level health info for debugging.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-24 08:51:34 -07:00
Brad Fitzpatrick
6d04184325 control/controlclient: add a noiseClient.post helper method
In prep for a future change that would've been very copy/paste-y.

And because the set-dns call doesn't currently use a context,
so timeouts/cancelations are plumbed.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-23 13:18:22 -07:00
Will Norris
8c72aabbdf licenses: remove win.md file
This was renamed to windows.md
2022-09-23 11:21:25 -07:00
James Tucker
f7cb535693 net/speedtest: retune to meet iperf on localhost in a VM
- removed some in-flow time calls
- increase buffer size to 2MB to overcome syscall cost
- move relative time computation from record to report time

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-23 10:46:04 -07:00
James Tucker
146f51ce76 net/packet: fix filtering of short IPv4 fragments
The fragment offset is an 8 byte offset rather than a byte offset, so
the short packet limit is now in fragment block size in order to compare
with the offset value.

The packet flags are in the first 3 bits of the flags/frags byte, and
so after conversion to a uint16 little endian value they are at the
start, not the end of the value - the mask for extracting "more
fragments" is adjusted to match this byte.

Extremely short fragments less than 80 bytes are dropped, but fragments
over 80 bytes are now accepted.

Fixes #5727

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-23 10:43:28 -07:00
Mihai Parparita
c66e15772f tsweb: consider 304s as successful for quiet logging
Static resource handlers will generate lots of 304s, which are
effectively successful responses.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-23 10:40:32 -07:00
Andrew Dunham
e1bdbfe710 tailcfg, control/controlhttp, control/controlclient: add ControlDialPlan field (#5648)
* tailcfg, control/controlhttp, control/controlclient: add ControlDialPlan field

This field allows the control server to provide explicit information
about how to connect to it; useful if the client's link status can
change after the initial connection, or if the DNS settings pushed by
the control server break future connections.

Change-Id: I720afe6289ec27d40a41b3dcb310ec45bd7e5f3e
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-23 13:06:55 -04:00
Aaron Klotz
acc7baac6d tailcfg, util/deephash: add DataPlaneAuditLogID to Node and DomainDataPlaneAuditLogID to MapResponse
We're adding two log IDs to facilitate data-plane audit logging: a node-specific
log ID, and a domain-specific log ID.

Updated util/deephash/deephash_test.go with revised expectations for tailcfg.Node.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-09-22 17:18:28 -06:00
Kyle Carberry
91794f6498 wgengine/magicsock: move firstDerp check after nil derpMap check
This fixes a race condition which caused `c.muCond.Broadcast()` to
never fire in the `firstDerp` if block. It resulted in `Close()`
hanging forever.

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2022-09-22 11:54:56 -07:00
Brad Fitzpatrick
2c447de6cc cmd/tailscaled: use explicit equal sign in --port=$PORT in tailscaled.service
Personal preference (so it's obvious it's not a bool flag), but it
also matches the --state= before it.

Bonus: stop allowing PORT to sneak in extra flags to be passed as
their own arguments, as $FOO and ${FOO} expand differently. (${FOO} is
required to concat to strings)

Change-Id: I994626a5663fe0948116b46a971e5eb2c4023216
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-22 11:54:22 -07:00
Anton Schubert
021bedfb89 docker: add ability to use a custom control socket
Signed-off-by: Anton Schubert <anton.schubert@riedel.net>
2022-09-22 08:48:26 -07:00
hlts2
d988c9f098 fix auth key name
Signed-off-by: hlts2 <hiroto.funakoshi.hiroto@gmail.com>
2022-09-22 03:55:05 -07:00
Andrew Dunham
0607832397 wgengine/netstack: always respond to 4via6 echo requests (#5712)
As the comment in the code says, netstack should always respond to ICMP
echo requests to a 4via6 address, even if the netstack instance isn't
normally processing subnet traffic.

Follow-up to #5709

Change-Id: I504d0776c5824071b2a2e0e687bc33e24f6c4746
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-21 18:07:57 -04:00
Will Norris
565dbc599a Revert "licenses: update win/apple licenses"
This reverts commit aadf63da1d.
2022-09-21 14:28:43 -07:00
License Updater
aadf63da1d licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-09-21 14:19:57 -07:00
Maisem Ali
d5781f61a9 ipn/ipnlocal: return usernames when Tailscale SSH is enabled
It was checking if the sshServer was initialized as a proxy, but that
could either not have been initialized yet or Tailscale SSH could have
been disabled after intialized.

Also bump tailcfg.CurrentCapabilityVersion

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-21 14:06:40 -07:00
Mihai Parparita
a7a0baf6b9 cmd/tsconnect: add error callback for SSH sessions
We were just logging them to the console, which is useful for debugging,
but we may want to show them in the UI too.

Updates tailscale/corp#6939

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-21 13:09:53 -07:00
Tom DNetto
e9b98dd2e1 control/controlclient,ipn/ipnlocal: wire tka enable/disable
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-21 12:57:59 -07:00
Andrew Dunham
b9b0bf65a0 wgengine/netstack: handle 4via6 packets when pinging (#5709)
Change-Id: Ib6ebbaa11219fb91b550ed7fc6ede61f83262e89
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-21 14:19:34 -04:00
Andrew Dunham
c6162c2a94 net/netcheck: add check for captive portal (#5593)
This doesn't change any behaviour for now, other than maybe running a
full netcheck more often. The intent is to start gathering data on
captive portals, and additionally, seeing this in the 'tailscale
netcheck' command should provide a bit of additional information to
users.

Updates #1634

Change-Id: I6ba08f9c584dc0200619fa97f9fde1a319f25c76
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-20 15:31:49 -04:00
Brad Fitzpatrick
aa5e494aba tsweb: export go_version in standard expvar vars
For monitoring.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-20 09:59:43 -07:00
Berk D. Demir
ff13c66f55 cmd/tailscale: fix configure-host command for Synology
d5e7e309 changed the `hostinfo.GetVersion` from distro and distro version
to UTS Name Release and moved distribution information under
`hostinfo.Distro*`.

`tailscale configure-host` command implementation for Synology DSM
environments relies on the old semantics of this string for matching DSM
Major version so it's been broken for a few days.

Pull in `hostinfo` and prefix match `hostinfo.DistroVersion` to match
DSM major version.

Signed-off-by: Berk D. Demir <bdd@mindcast.org>
2022-09-19 21:15:21 -07:00
Brad Fitzpatrick
ed248b04a7 cmd/tailscale: remove leftover debug prints from earlier commit
From 6632504f45

Change-Id: If21789232b3ecc14c1639cf87814af6fa73f535f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-19 21:13:56 -07:00
Mihai Parparita
8158dd2edc cmd/tsconnect: allow SSH connection timeout to be overridden
5 seconds may not be enough if we're still loading the derp map and
connecting to a slow machine.

Updates #5693

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-19 18:00:12 -07:00
Maisem Ali
6632504f45 cmd/tailscale/cli: [up] move lose-ssh check after other validations
The check was happening too early and in the case of error would wait 5
s and then error out. This makes it so that it does validations before
the SSH check.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 12:04:14 -07:00
Maisem Ali
054ef4de56 tailcfg: mark CapabilityFileSharingTarget as inter-node
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 11:08:34 -07:00
Brad Fitzpatrick
d045462dfb ipn/ipnlocal: add c2n method to get SSH username candidates
For control to fetch a list of Tailscale SSH username candidates to
filter against the Tailnet's SSH policy to present some valid
candidates to a user.

Updates #3802
Updates tailscale/corp#7007

Change-Id: I3dce57b7a35e66891d5e5572e13ae6ef3c898498
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-19 10:37:04 -07:00
Brad Fitzpatrick
d8eb111ac8 .github/workflows: add cross-android
This would've caught the regression from 7c49db02a before it was
submitted so 42f1d92ae0 wouldn't have been necessary to fix it.

Updates #4482

Change-Id: Ia4a9977e21853f68df96f043672c86a86c0181db
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 09:56:19 -07:00
Brad Fitzpatrick
832031d54b wgengine/magicsock: fix recently introduced data race
From 5c42990c2f, not yet released in a stable build.
Caught by existing tests.

Fixes #5685

Change-Id: Ia76bb328809d9644e8b96910767facf627830600
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 08:07:57 -07:00
Denton Gentry
42f1d92ae0 net/netns: implement UseSocketMark for Android.
Build fails on Android:
`../../../../go/pkg/mod/tailscale.com@v1.1.1-0.20220916223019-65c24b6334e9/wgengine/magicsock/magicsock_linux.go:133:12: undefined: netns.UseSocketMark`

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-09-17 23:19:24 -07:00
Brad Fitzpatrick
41bb47de0e cmd/tailscaled: respect $PORT on all platforms, not just Linux
Updates #5114

Change-Id: I6c6e28c493d6a026a03088157d08f9fd182ef373
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 12:30:29 -07:00
Brad Fitzpatrick
3562b5bdfa envknob, health: support Synology, show parse errors in status
Updates #5114

Change-Id: I8ac7a22a511f5a7d0dcb8cac470d4a403aa8c817
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 08:42:41 -07:00
phirework
5c42990c2f wgengine/magicsock: add client flag and envknob to disable heartbeat (#5638)
Baby steps towards turning off heartbeat pings entirely as per #540.
This doesn't change any current magicsock functionality and requires additional
changes to send/disco paths before the flag can be turned on.

Updates #540

Change-Id: Idc9a72748e74145b068d67e6dd4a4ffe3932efd0
Signed-off-by: Jenny Zhang <jz@tailscale.com>

Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-09-16 23:48:46 -04:00
Brad Fitzpatrick
65c24b6334 envknob: generalize Windows tailscaled-env.txt support
ipnserver previously had support for a Windows-only environment
variable mechanism that further only worked when Windows was running
as a service, not from a console.

But we want it to work from tailscaed too, and we want it to work on
macOS and Synology. So move it to envknob, now that envknob can change
values at runtime post-init.

A future change will wire this up for more platforms, and do something
more for CLI flags like --port, which the bug was originally about.

Updates #5114

Change-Id: I9fd69a9a91bb0f308fc264d4a6c33e0cbe352d71
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 15:30:19 -07:00
Brad Fitzpatrick
4bda41e701 Dockerfile: add test that build-env Alpine version matches go.mod
So things like #5660 don't happen in the future.

Change-Id: I01234f241e297d5b7bdd18da1bb3cc5420ad2225
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 12:19:09 -07:00
Andrew Dunham
9b71008ef2 control/controlhttp: move Dial options into options struct (#5661)
This turns 'dialParams' into something more like net.Dialer, where
configuration fields are public on the struct.

Split out of #5648

Change-Id: I0c56fd151dc5489c3c94fb40d18fd639e06473bc
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-16 15:06:25 -04:00
Luis Peralta
5623ef0271 Update Dockerfile to use golang:1.19-alpine
Tailscale @4a82b31 does not build in the container image due to using golang:1.18 image

Signed-off-by: Luis Peralta <luis.peralta@gmail.com>
2022-09-16 11:40:31 -07:00
Tyler Lee
486eecc063 Switched Secret snippet to match run.sh
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Tyler Lee
b830c9975f Updated secret example in readme to match the sidecar key value
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Brad Fitzpatrick
4a82b317b7 ipn/{ipnlocal,localapi}: use strs.CutPrefix, add more domain validation
The GitHub CodeQL scanner flagged the localapi's cert domain usage as a problem
because user input in the URL made it to disk stat checks.

The domain is validated against the ipnstate.Status later, and only
authenticated root/configured users can hit this, but add some
paranoia anyway.

Change-Id: I373ef23832f1d8b3a27208bc811b6588ae5a1ddd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 05:52:33 -07:00
Eng Zer Jun
f0347e841f refactor: move from io/ioutil to io and os packages
The io/ioutil package has been deprecated as of Go 1.16 [1]. This commit
replaces the existing io/ioutil functions with their new definitions in
io and os packages.

Reference: https://golang.org/doc/go1.16#ioutil
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2022-09-15 21:45:53 -07:00
Mihai Parparita
027111fb5a derp: update DERP acronym expansion
Makes the package description consistent with other documentation.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 16:08:12 -07:00
Mihai Parparita
1ce0e558a7 cmd/derper, control/controlhttp: disable WebSocket compression
The data that we send over WebSockets is encrypted and thus not
compressible. Additionally, Safari has a broken implementation of compression
(see nhooyr/websocket#218) that makes enabling it actively harmful.

Fixes tailscale/corp#6943

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 15:35:49 -07:00
Brad Fitzpatrick
74674b110d envknob: support changing envknobs post-init
Updates #5114

Change-Id: Ia423fc7486e1b3f3180a26308278be0086fae49b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 15:04:02 -07:00
Brad Fitzpatrick
33ee2c058e wgengine: update comments, remove redundant code in forceFullWireguardConfig
Change-Id: I464a0bce36e3a362c7d7ace0e8d2dd77fa825ee2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 13:03:18 -07:00
Brad Fitzpatrick
d34dd43562 ipn/ipnlocal: remove unused envknob
Change-Id: I6d18af2c469eb660e6ca81d1dcc2af33c9e628aa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 11:06:15 -07:00
157 changed files with 3041 additions and 687 deletions

View File

@@ -1,5 +1,7 @@
name: CIFuzz
on: [pull_request]
on:
push:
branches: [ main, release-branch/* ]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -7,7 +9,7 @@ concurrency:
jobs:
Fuzzing:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: Build Fuzzers
id: build
@@ -20,7 +22,7 @@ jobs:
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 300
fuzz-seconds: 900
dry-run: false
language: go
- name: Upload Crash

View File

@@ -27,7 +27,7 @@ concurrency:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
permissions:
actions: read
contents: read

54
.github/workflows/cross-android.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Android-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Android smoke build
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
# some Android breakages early.
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
env:
GOOS: android
GOARCH: arm64
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -38,10 +38,6 @@ jobs:
- name: Get QEMU
run: |
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
# use this PPA which brings in a modern qemu.
sudo add-apt-repository -y ppa:jacob/virtualisation
sudo apt-get -y update
sudo apt-get -y install qemu-user

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -40,7 +40,7 @@ jobs:
if: failure() && github.event_name == 'push'
vet:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: Set up Go
uses: actions/setup-go@v3
@@ -66,7 +66,7 @@ jobs:
if: failure() && github.event_name == 'push'
staticcheck:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
strategy:
matrix:
goos: [linux, windows, darwin]

View File

@@ -11,7 +11,7 @@ concurrency:
jobs:
ubuntu2004-LTS-cloud-base:
runs-on: [ self-hosted, linux, vm ]
runs-on: buildjet-4vcpu-ubuntu-2204
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -27,12 +27,17 @@ jobs:
with:
go-version-file: go.mod
- name: Get QEMU
run: |
sudo apt-get -y update
sudo apt-get -y install qemu-system-x86 qemu-user genisoimage qemu-utils
- name: Run VM tests
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
env:
HOME: "/tmp"
TMPDIR: "/tmp"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
XDG_CACHE_HOME: "/home/runner/cache"
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
test:
runs-on: windows-latest
runs-on: windows-8vcpu
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -32,7 +32,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.18-alpine AS build-env
FROM golang:1.19-alpine AS build-env
WORKDIR /go/src/tailscale

View File

@@ -9,7 +9,6 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -18,7 +17,7 @@ import (
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}

View File

@@ -15,7 +15,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
@@ -137,7 +136,7 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
onVersionMismatch(ipn.IPCVersion(), server)
}
if res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
all, _ := io.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
@@ -207,7 +206,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
slurp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
@@ -365,7 +364,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}

View File

@@ -17,7 +17,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
)
@@ -131,7 +130,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
// Read response. Limit the response to 10MB.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := ioutil.ReadAll(body)
b, err := io.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}

View File

@@ -51,7 +51,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale
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

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
@@ -99,7 +98,7 @@ func loadConfig() config {
}
log.Printf("no config path specified; using %s", *configPath)
}
b, err := ioutil.ReadFile(*configPath)
b, err := os.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
return writeNewConfig()
@@ -155,7 +154,7 @@ func main() {
s.SetVerifyClient(*verifyClients)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
b, err := os.ReadFile(*meshPSKFile)
if err != nil {
log.Fatal(err)
}

View File

@@ -33,6 +33,12 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because we transmit WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Printf("websocket.Accept: %v", err)

View File

@@ -13,7 +13,6 @@ import (
"errors"
"flag"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
@@ -106,7 +105,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
tmplData, err := os.ReadFile("hello.tmpl.html")
if os.IsNotExist(err) {
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
return tmpl, nil

View File

@@ -110,11 +110,12 @@ func runSpeedtest(ctx context.Context, args []string) error {
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
fmt.Println("Results:")
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
startTime := results[0].IntervalStart
for _, r := range results {
if r.Total {
fmt.Fprintln(w, "-------------------------------------------------------------------------")
}
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
}
w.Flush()
return nil

View File

@@ -789,6 +789,10 @@ func TestUpdatePrefs(t *testing.T) {
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
// It is used to test the --accept-risks flag.
sshOverTailscale bool
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
// updatePrefs might've mutated them (from applyImplicitPrefs).
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
@@ -916,15 +920,159 @@ func TestUpdatePrefs(t *testing.T) {
}
},
},
{
name: "enable_ssh",
flags: []string{"--ssh"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh",
flags: []string{"--ssh=false"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
runSSH: true,
}},
},
{
name: "disable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=false"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
RunSSH: true,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=true"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh",
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh_over_ssh",
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
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 })
}
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
flags := CleanUpArgs(tt.flags)
tt.env.flagSet.Parse(flags)
if err := tt.env.flagSet.Parse(flags); err != nil {
t.Fatal(err)
}
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
@@ -939,6 +1087,8 @@ func TestUpdatePrefs(t *testing.T) {
return
}
t.Fatal(err)
} else if tt.wantErrSubtr != "" {
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
}
if tt.checkUpdatePrefsMutations != nil {
tt.checkUpdatePrefsMutations(t, newPrefs)
@@ -952,13 +1102,18 @@ func TestUpdatePrefs(t *testing.T) {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
}
})
}
}
func asJSON(v any) string {
b, _ := json.MarshalIndent(v, "", "\t")
return string(b)
}
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
return a == b
})

View File

@@ -48,11 +48,11 @@ func runConfigureHost(ctx context.Context, args []string) error {
if uid := os.Getuid(); uid != 0 {
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
}
osVer := hostinfo.GetOSVersion()
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
hi:= hostinfo.New()
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
if !isDSM6 && !isDSM7 {
return fmt.Errorf("unsupported DSM version %q", osVer)
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
}
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if err := os.MkdirAll("/dev/net", 0755); err != nil {

View File

@@ -489,7 +489,15 @@ func runTS2021(ctx context.Context, args []string) error {
return c, err
}
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
conn, err := (&controlhttp.Dialer{
Hostname: ts2021Args.host,
HTTPPort: "80",
HTTPSPort: "443",
MachineKey: machinePrivate,
ControlKey: keys.PublicKey,
ProtocolVersion: uint16(ts2021Args.version),
Dialer: dialFunc,
}).Dial(ctx)
log.Printf("controlhttp.Dial = %p, %v", conn, err)
if err != nil {
return err

View File

@@ -22,9 +22,13 @@ var downCmd = &ffcli.Command{
FlagSet: newDownFlagSet(),
}
var downArgs struct {
acceptedRisks string
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
registerAcceptRiskFlag(downf)
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
return downf
}
@@ -34,7 +38,7 @@ func runDown(ctx context.Context, args []string) error {
}
if isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
return err
}
}

View File

@@ -10,7 +10,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"sort"
@@ -134,6 +133,9 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
printf("\t* HairPinning: %v\n", report.HairPinning)
printf("\t* PortMapping: %v\n", portMapping(report))
if report.CaptivePortal != "" {
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
}
// When DERP latency checking failed,
// magicsock will try to pick the DERP server that
@@ -202,7 +204,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}

View File

@@ -16,9 +16,8 @@ import (
)
var (
riskTypes []string
acceptedRisks string
riskLoseSSH = registerRiskType("lose-ssh")
riskTypes []string
riskLoseSSH = registerRiskType("lose-ssh")
)
func registerRiskType(riskType string) string {
@@ -28,12 +27,13 @@ func registerRiskType(riskType string) string {
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
// in presentRiskToUser.
func registerAcceptRiskFlag(f *flag.FlagSet) {
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
}
// riskAccepted reports whether riskType is in acceptedRisks.
func riskAccepted(riskType string) bool {
// isRiskAccepted reports whether riskType is in the comma-separated list of
// risks in acceptedRisks.
func isRiskAccepted(riskType, acceptedRisks string) bool {
for _, r := range strings.Split(acceptedRisks, ",") {
if r == riskType {
return true
@@ -49,12 +49,16 @@ var errAborted = errors.New("aborted, no changes made")
// It is used by the presentRiskToUser function below.
const riskAbortTimeSeconds = 5
// presentRiskToUser displays the risk message and waits for the user to
// cancel. It returns errorAborted if the user aborts.
func presentRiskToUser(riskType, riskMessage string) error {
if riskAccepted(riskType) {
// presentRiskToUser displays the risk message and waits for the user to cancel.
// It returns errorAborted if the user aborts. In tests it returns errAborted
// immediately unless the risk has been explicitly accepted.
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
if isRiskAccepted(riskType, acceptedRisks) {
return nil
}
if inTest() {
return errAborted
}
outln(riskMessage)
printf("To skip this warning, use --accept-risk=%s\n", riskType)

View File

@@ -116,7 +116,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
registerAcceptRiskFlag(upf)
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
return upf
}
@@ -150,6 +150,7 @@ type upArgsT struct {
opUser string
json bool
timeout time.Duration
acceptedRisks string
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -376,6 +377,20 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
}
// Do this after validations to avoid the 5s delay if we're going to error
// out anyway.
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
if wantSSH != haveSSH && isSSHOverTailscale() {
if wantSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
}
if err != nil {
return false, nil, err
}
}
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
simpleUp = env.flagSet.NFlag() == 0 &&
@@ -475,17 +490,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
curExitNodeIP: exitNodeIP(curPrefs, st),
}
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
if upArgs.runSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
}
if err != nil {
return err
}
}
defer func() {
if retErr == nil {
checkSSHUpWarnings(ctx)

View File

@@ -15,7 +15,6 @@ import (
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -254,7 +253,7 @@ func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
return "", nil, err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
out, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}

View File

@@ -100,6 +100,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/lineread from tailscale.com/net/interfaces+
tailscale.com/util/mak from tailscale.com/net/netcheck
tailscale.com/util/multierr from tailscale.com/control/controlhttp
tailscale.com/util/singleflight from tailscale.com/net/dnscache
L tailscale.com/util/strs from tailscale.com/hostinfo
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+

View File

@@ -15,7 +15,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -173,7 +172,7 @@ func checkDerp(ctx context.Context, derpRegion string) error {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -142,7 +141,7 @@ func installSystemDaemonDarwin(args []string) (err error) {
return err
}
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}

View File

@@ -26,6 +26,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
@@ -97,6 +98,20 @@ func defaultTunName() string {
return "tailscale0"
}
// defaultPort returns the default UDP port to listen on for disco+wireguard.
// By default it returns 0, to pick one randomly from the kernel.
// If the environment variable PORT is set, that's used instead.
// The PORT environment variable is chosen to match what the Linux systemd
// unit uses, to make documentation more consistent.
func defaultPort() uint16 {
if s := envknob.String("PORT"); s != "" {
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
return uint16(p)
}
}
return 0
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
@@ -132,6 +147,9 @@ var subCommands = map[string]*func([]string) error{
var beCLI func() // non-nil if CLI is linked in
func main() {
envknob.PanicIfAnyEnvCheckedInInit()
envknob.ApplyDiskConfig()
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
@@ -139,7 +157,7 @@ func main() {
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
@@ -308,6 +326,10 @@ func run() error {
pol.Shutdown(ctx)
}()
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
@@ -376,7 +398,7 @@ func run() error {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
if useNetstack {
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
@@ -477,8 +499,6 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
return nil, false, multierr.New(errs...)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
return v
@@ -549,7 +569,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
}
conf.DNS = d
conf.Router = r
if wrapNetstack {
if shouldWrapNetstack() {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}

View File

@@ -7,7 +7,7 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStartPre=/usr/sbin/tailscaled --cleanup
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup
Restart=on-failure

View File

@@ -197,6 +197,9 @@ func beWindowsSubprocess() bool {
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
go func() {
b := make([]byte, 16)
@@ -274,7 +277,7 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, nil, fmt.Errorf("router: %w", err)
}
if wrapNetstack {
if shouldWrapNetstack() {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
@@ -301,7 +304,7 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
ns.ProcessSubnets = shouldWrapNetstack()
if err := ns.Start(); err != nil {
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
}

View File

@@ -7,7 +7,6 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path"
@@ -47,7 +46,7 @@ func runBuild() {
if err != nil {
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
}
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
log.Fatalf("Cannot write metadata: %v", err)
}

View File

@@ -6,7 +6,6 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
@@ -183,7 +182,7 @@ func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
func buildWasm(dev bool) ([]byte, error) {
start := time.Now()
outputFile, err := ioutil.TempFile("", "main.*.wasm")
outputFile, err := os.CreateTemp("", "main.*.wasm")
if err != nil {
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
@@ -75,7 +74,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
}
defer esbuildMetadataFile.Close()
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
if err != nil {
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
}

View File

@@ -46,7 +46,7 @@ function SSHSession({
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
runSSHSession(ref.current, def, ipn, onDone)
runSSHSession(ref.current, def, ipn, onDone, (err) => console.error(err))
}
}, [ref])

View File

@@ -5,6 +5,8 @@ import { WebLinksAddon } from "xterm-addon-web-links"
export type SSHSessionDef = {
username: string
hostname: string
/** Defaults to 5 seconds */
timeoutSeconds?: number
}
export function runSSHSession(
@@ -12,6 +14,7 @@ export function runSSHSession(
def: SSHSessionDef,
ipn: IPN,
onDone: () => void,
onError?: (err: string) => void,
terminalOptions?: ITerminalOptions
) {
const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
@@ -46,7 +49,7 @@ export function runSSHSession(
term.write(input)
},
writeErrorFn(err) {
console.error(err)
onError?.(err)
term.write(err)
},
setReadFn(hook) {
@@ -62,6 +65,7 @@ export function runSSHSession(
}
onDone()
},
timeoutSeconds: def.timeoutSeconds,
})
// Make terminal and SSH session track the size of the containing DOM node.

View File

@@ -23,6 +23,8 @@ declare global {
setReadFn: (readFn: (data: string) => void) => void
rows: number
cols: number
/** Defaults to 5 seconds */
timeoutSeconds?: number
onDone: () => void
}
): IPNSSHSession

View File

@@ -360,6 +360,10 @@ func (s *jsSSHSession) Run() {
setReadFn := s.termConfig.Get("setReadFn")
rows := s.termConfig.Get("rows").Int()
cols := s.termConfig.Get("cols").Int()
timeoutSeconds := 5.0
if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
timeoutSeconds = jsTimeoutSeconds.Float()
}
onDone := s.termConfig.Get("onDone")
defer onDone.Invoke()
@@ -367,7 +371,7 @@ func (s *jsSSHSession) Run() {
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
defer cancel()
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
if err != nil {

View File

@@ -114,19 +114,11 @@ func NewNoStart(opts Options) (*Auto, error) {
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
return c, nil
}
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
if sys == health.SysOverall {
return
}
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
c.cancelMapSafely()
}
// SetPaused controls whether HTTP activity should be paused.
//
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@@ -77,6 +76,8 @@ type Direct struct {
popBrowser func(url string) // or nil
c2nHandler http.Handler // or nil
dialPlan ControlDialPlanner // can be nil
mu sync.Mutex // mutex guards the following fields
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
serverNoiseKey key.MachinePublic
@@ -107,6 +108,7 @@ type Options struct {
KeepAlive bool
Logf logger.Logf
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
PopBrowserURL func(url string) // optional func to open browser
@@ -133,6 +135,34 @@ type Options struct {
// MapResponse.PingRequest queries from the control plane.
// If nil, PingRequest queries are not answered.
Pinger Pinger
// DialPlan contains and stores a previous dial plan that we received
// from the control server; if nil, we fall back to using DNS.
//
// If we receive a new DialPlan from the server, this value will be
// updated.
DialPlan ControlDialPlanner
}
// ControlDialPlanner is the interface optionally supplied when creating a
// control client to control exactly how TCP connections to the control plane
// are dialed.
//
// It is usually implemented by an atomic.Pointer.
type ControlDialPlanner interface {
// Load returns the current plan for how to connect to control.
//
// The returned plan can be nil. If so, connections should be made by
// resolving the control URL using DNS.
Load() *tailcfg.ControlDialPlan
// Store updates the dial plan with new directions from the control
// server.
//
// The dial plan can span multiple connections to the control server.
// That is, a dial plan received when connected over Wi-Fi is still
// valid for a subsequent connection over LTE after a network switch.
Store(*tailcfg.ControlDialPlan)
}
// Pinger is the LocalBackend.Ping method.
@@ -216,6 +246,7 @@ func NewDirect(opts Options) (*Direct, error) {
popBrowser: opts.PopBrowserURL,
c2nHandler: opts.C2NHandler,
dialer: opts.Dialer,
dialPlan: opts.DialPlan,
}
if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New())
@@ -227,6 +258,12 @@ func NewDirect(opts Options) (*Direct, error) {
c.SetNetInfo(ni)
}
}
if opts.NoiseTestClient != nil {
c.noiseClient = &noiseClient{
Client: opts.NoiseTestClient,
}
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
}
return c, nil
}
@@ -490,7 +527,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister {
if debugRegister() {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
@@ -523,7 +560,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
return regen, opt.URL, fmt.Errorf("register request: %w", err)
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -533,7 +570,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if debugRegister {
if debugRegister() {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
@@ -715,7 +752,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
vlogf := logger.Discard
if DevKnob.DumpNetMaps {
if DevKnob.DumpNetMaps() {
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
// want to upload it always.
vlogf = c.logf
@@ -804,7 +841,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
}
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -814,7 +851,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
health.NoteMapRequestHeard(request)
if cb == nil {
io.Copy(ioutil.Discard, res.Body)
io.Copy(io.Discard, res.Body)
return nil
}
@@ -909,6 +946,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
} else {
vlogf("netmap: got new map")
}
if resp.ControlDialPlan != nil {
if c.dialPlan != nil {
c.logf("netmap: got new dial plan from control")
c.dialPlan.Store(resp.ControlDialPlan)
} else {
c.logf("netmap: [unexpected] new dial plan; nowhere to store it")
}
}
select {
case timeoutReset <- struct{}{}:
@@ -963,12 +1008,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
controlTrimWGConfig.Store(d.TrimWGConfig)
}
if DevKnob.StripEndpoints {
if DevKnob.StripEndpoints() {
for _, p := range resp.Peers {
p.Endpoints = nil
}
}
if DevKnob.StripCaps {
if DevKnob.StripCaps() {
nm.SelfNode.Capabilities = nil
}
@@ -998,7 +1043,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
defer res.Body.Close()
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return err
}
@@ -1012,8 +1057,8 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
}
var (
debugMap = envknob.Bool("TS_DEBUG_MAP")
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
)
var jsonEscapedZero = []byte(`\u0000`)
@@ -1051,7 +1096,7 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
return err
}
}
if debugMap {
if debugMap() {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
@@ -1088,7 +1133,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
if err != nil {
return nil, err
}
if debugMap {
if debugMap() {
if _, ok := v.(*tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -1110,7 +1155,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
return nil, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 64<<10))
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
if err != nil {
return nil, fmt.Errorf("fetch control key response: %v", err)
}
@@ -1139,18 +1184,18 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
var DevKnob = initDevKnob()
type devKnobs struct {
DumpNetMaps bool
ForceProxyDNS bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
DumpNetMaps func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
StripCaps func() bool // strip all local node's control-provided capabilities
}
func initDevKnob() devKnobs {
return devKnobs{
DumpNetMaps: envknob.Bool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
}
}
@@ -1359,12 +1404,17 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
if nc != nil {
return nc, nil
}
var dp func() *tailcfg.ControlDialPlan
if c.dialPlan != nil {
dp = c.dialPlan.Load
}
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
k, err := c.getMachinePrivKey()
if err != nil {
return nil, err
}
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
c.logf("creating new noise client")
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
if err != nil {
return nil, err
}
@@ -1384,21 +1434,17 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) error {
newReq := *req
newReq.Version = tailcfg.CurrentCapabilityVersion
np, err := c.getNoiseClient()
nc, err := c.getNoiseClient()
if err != nil {
return err
}
bodyData, err := json.Marshal(newReq)
if err != nil {
return err
}
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
res, err := nc.post(ctx, "/machine/set-dns", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse
@@ -1464,7 +1510,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse
@@ -1540,6 +1586,38 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
return nil
}
// ReportHealthChange reports to the control plane a change to this node's
// health.
func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
if sys == health.SysOverall {
// We don't report these. These include things like the network is down
// (in which case we can't report anyway) or the user wanted things
// stopped, as opposed to the more unexpected failure types in the other
// subsystems.
return
}
np, err := c.getNoiseClient()
if err != nil {
// Don't report errors to control if the server doesn't support noise.
return
}
req := &tailcfg.HealthChangeRequest{
Subsys: string(sys),
}
if sysErr != nil {
req.Error = sysErr.Error()
}
// Best effort, no logging:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
res, err := np.post(ctx, "/machine/update-health", req)
if err != nil {
return
}
res.Body.Close()
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")

View File

@@ -48,6 +48,7 @@ type mapSession struct {
lastHealth []string
lastPopBrowserURL string
stickyDebug tailcfg.Debug // accumulated opt.Bool values
lastTKAInfo *tailcfg.TKAInfo
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.
@@ -115,6 +116,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.Health != nil {
ms.lastHealth = resp.Health
}
if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo
}
debug := resp.Debug
if debug != nil {
@@ -152,9 +156,17 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
}
ms.netMapBuilding = nm
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
ms.logf("error unmarshalling TKAHead: %v", err)
nm.TKAEnabled = false
}
}
if resp.Node != nil {
ms.lastNode = resp.Node
}
@@ -190,7 +202,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
ms.addUserProfile(peer.User)
}
if DevKnob.ForceProxyDNS {
if DevKnob.ForceProxyDNS() {
nm.DNS.Proxied = true
}
ms.netMapBuilding = nil
@@ -356,13 +368,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
return v2
}
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
switch {
default:
return in
case debugSelfIPv6Only:
case debugSelfIPv6Only():
for _, a := range in {
if a.Addr().Is6() {
ret = append(ret, a)

View File

@@ -5,8 +5,10 @@
package controlclient
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"math"
"net"
"net/http"
@@ -53,6 +55,11 @@ type noiseClient struct {
httpPort string // the default port to call
httpsPort string // the fallback Noise-over-https port
// dialPlan optionally returns a ControlDialPlan previously received
// from the control server; either the function or the return value can
// be nil.
dialPlan func() *tailcfg.ControlDialPlan
// mu only protects the following variables.
mu sync.Mutex
nextID int
@@ -61,7 +68,9 @@ type noiseClient struct {
// newNoiseClient returns a new noiseClient for the provided server and machine key.
// serverURL is of the form https://<host>:<port> (no trailing slash).
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
//
// dialPlan may be nil
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
@@ -89,6 +98,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
httpPort: httpPort,
httpsPort: httpsPort,
dialer: dialer,
dialPlan: dialPlan,
}
// Create the HTTP/2 Transport using a net/http.Transport
@@ -155,17 +165,61 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
nc.nextID++
nc.mu.Unlock()
// Timeout is a little arbitrary, but plenty long enough for even the
// highest latency links.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
// Panic, because a test should have started failing several
// thousand version numbers before getting to this point.
panic("capability version is too high to fit in the wire protocol")
}
conn, err := controlhttp.Dial(ctx, nc.host, nc.httpPort, nc.httpsPort, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
var dialPlan *tailcfg.ControlDialPlan
if nc.dialPlan != nil {
dialPlan = nc.dialPlan()
}
// If we have a dial plan, then set our timeout as slightly longer than
// the maximum amount of time contained therein; we assume that
// explicit instructions on timeouts are more useful than a single
// hard-coded timeout.
//
// The default value of 5 is chosen so that, when there's no dial plan,
// we retain the previous behaviour of 10 seconds end-to-end timeout.
timeoutSec := 5.0
if dialPlan != nil {
for _, c := range dialPlan.Candidates {
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
timeoutSec = v
}
}
}
// After we establish a connection, we need some time to actually
// upgrade it into a Noise connection. With a ballpark worst-case RTT
// of 1000ms, give ourselves an extra 5 seconds to complete the
// handshake.
timeoutSec += 5
// Be extremely defensive and ensure that the timeout is in the range
// [5, 60] seconds (e.g. if we accidentally get a negative number).
if timeoutSec > 60 {
timeoutSec = 60
} else if timeoutSec < 5 {
timeoutSec = 5
}
timeout := time.Duration(timeoutSec * float64(time.Second))
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := (&controlhttp.Dialer{
Hostname: nc.host,
HTTPPort: nc.httpPort,
HTTPSPort: nc.httpsPort,
MachineKey: nc.privKey,
ControlKey: nc.serverPubKey,
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
DialPlan: dialPlan,
}).Dial(ctx)
if err != nil {
return nil, err
}
@@ -176,3 +230,16 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
mak.Set(&nc.connPool, ncc.id, ncc)
return ncc, nil
}
func (nc *noiseClient) post(ctx context.Context, path string, body any) (*http.Response, error) {
jbody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return nc.Do(req)
}

View File

@@ -28,69 +28,231 @@ import (
"errors"
"fmt"
"io"
"math"
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"sort"
"sync/atomic"
"time"
"tailscale.com/control/controlbase"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/types/key"
"tailscale.com/tailcfg"
"tailscale.com/util/multierr"
)
// Dial connects to the HTTP server at host:httpPort, requests to switch to the
// Tailscale control protocol, and returns an established control
var stdDialer net.Dialer
// Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
// switch to the Tailscale control protocol, and returns an established control
// protocol connection.
//
// If Dial fails to connect using addr, it also tries to tunnel over
// TLS to host:httpsPort as a compatibility fallback.
// If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
// Dialer's Host:HTTPSPort as a compatibility fallback.
//
// The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established.
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
a := &dialParams{
host: host,
httpPort: httpPort,
httpsPort: httpsPort,
machineKey: machineKey,
controlKey: controlKey,
version: protocolVersion,
proxyFunc: tshttpproxy.ProxyFromEnvironment,
dialer: dialer,
func (a *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if a.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
return a.dial(ctx)
}
type dialParams struct {
host string
httpPort string
httpsPort string
machineKey key.MachinePrivate
controlKey key.MachinePublic
version uint16
proxyFunc func(*http.Request) (*url.URL, error) // or nil
dialer dnscache.DialContextFunc
// For tests only
insecureTLS bool
testFallbackDelay time.Duration
func (a *Dialer) logf(format string, args ...any) {
if a.Logf != nil {
a.Logf(format, args...)
}
}
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
// starting to try a.httpsPort.
func (a *dialParams) httpsFallbackDelay() time.Duration {
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
if a.proxyFunc != nil {
return a.proxyFunc
}
return tshttpproxy.ProxyFromEnvironment
}
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
// starting to try a.HTTPSPort.
func (a *Dialer) httpsFallbackDelay() time.Duration {
if v := a.testFallbackDelay; v != 0 {
return v
}
return 500 * time.Millisecond
}
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
// If we don't have a dial plan, just fall back to dialing the single
// host we know about.
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
return a.dialHost(ctx, netip.Addr{})
}
candidates := a.DialPlan.Candidates
// Otherwise, we try dialing per the plan. Store the highest priority
// in the list, so that if we get a connection to one of those
// candidates we can return quickly.
var highestPriority int = math.MinInt
for _, c := range candidates {
if c.Priority > highestPriority {
highestPriority = c.Priority
}
}
// This context allows us to cancel in-flight connections if we get a
// highest-priority connection before we're all done.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Now, for each candidate, kick off a dial in parallel.
type dialResult struct {
conn *controlbase.Conn
err error
addr netip.Addr
priority int
}
resultsCh := make(chan dialResult, len(candidates))
var pending atomic.Int32
pending.Store(int32(len(candidates)))
for _, c := range candidates {
go func(ctx context.Context, c tailcfg.ControlIPCandidate) {
var (
conn *controlbase.Conn
err error
)
// Always send results back to our channel.
defer func() {
resultsCh <- dialResult{conn, err, c.IP, c.Priority}
if pending.Add(-1) == 0 {
close(resultsCh)
}
}()
// If non-zero, wait the configured start timeout
// before we do anything.
if c.DialStartDelaySec > 0 {
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
defer tmr.Stop()
select {
case <-ctx.Done():
err = ctx.Err()
return
case <-tmr.C:
}
}
// Now, create a sub-context with the given timeout and
// try dialing the provided host.
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.DialTimeoutSec*float64(time.Second)))
defer cancel()
// This will dial, and the defer above sends it back to our parent.
a.logf("[v2] controlhttp: trying to dial %q @ %v", a.Hostname, c.IP)
conn, err = a.dialHost(ctx, c.IP)
}(ctx, c)
}
var results []dialResult
for res := range resultsCh {
// If we get a response that has the highest priority, we don't
// need to wait for any of the other connections to finish; we
// can just return this connection.
//
// TODO(andrew): we could make this better by keeping track of
// the highest remaining priority dynamically, instead of just
// checking for the highest total
if res.priority == highestPriority && res.conn != nil {
a.logf("[v1] controlhttp: high-priority success dialing %q @ %v from dial plan", a.Hostname, res.addr)
// Drain the channel and any existing connections in
// the background.
go func() {
for _, res := range results {
if res.conn != nil {
res.conn.Close()
}
}
for res := range resultsCh {
if res.conn != nil {
res.conn.Close()
}
}
if a.drainFinished != nil {
close(a.drainFinished)
}
}()
return res.conn, nil
}
// This isn't a highest-priority result, so just store it until
// we're done.
results = append(results, res)
}
// After we finish this function, close any remaining open connections.
defer func() {
for _, result := range results {
// Note: below, we nil out the returned connection (if
// any) in the slice so we don't close it.
if result.conn != nil {
result.conn.Close()
}
}
// We don't drain asynchronously after this point, so notify our
// channel when we return.
if a.drainFinished != nil {
close(a.drainFinished)
}
}()
// Sort by priority, then take the first non-error response.
sort.Slice(results, func(i, j int) bool {
// NOTE: intentionally inverted so that the highest priority
// item comes first
return results[i].priority > results[j].priority
})
var (
conn *controlbase.Conn
errs []error
)
for i, result := range results {
if result.err != nil {
errs = append(errs, result.err)
continue
}
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, result.addr)
conn = result.conn
results[i].conn = nil // so we don't close it in the defer
return conn, nil
}
merr := multierr.New(errs...)
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", merr.Error())
return a.dialHost(ctx, netip.Addr{})
}
// dialHost connects to the configured Dialer.Hostname and upgrades the
// connection into a controlbase.Conn. If addr is valid, then no DNS is used
// and the connection will be made to the provided address.
func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Conn, error) {
// Create one shared context used by both port 80 and port 443 dials.
// If port 80 is still in flight when 443 returns, this deferred cancel
// will stop the port 80 dial.
@@ -102,12 +264,12 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
// we'll speak Noise.
u80 := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(a.host, a.httpPort),
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
Path: serverUpgradePath,
}
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.host, a.httpsPort),
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
Path: serverUpgradePath,
}
@@ -118,7 +280,7 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
}
ch := make(chan tryURLRes) // must be unbuffered
try := func(u *url.URL) {
cbConn, err := a.dialURL(ctx, u)
cbConn, err := a.dialURL(ctx, u, addr)
select {
case ch <- tryURLRes{u, cbConn, err}:
case <-ctx.Done():
@@ -169,12 +331,12 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
}
// dialURL attempts to connect to the given URL.
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
if err != nil {
return nil, err
}
netConn, err := a.tryURLUpgrade(ctx, u, init)
netConn, err := a.tryURLUpgrade(ctx, u, addr, init)
if err != nil {
return nil, err
}
@@ -186,29 +348,50 @@ func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn
return cbConn, nil
}
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
// 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.
//
// Only the provided ctx is used, not a.ctx.
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
dns := &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (net.Conn, error) {
var dns *dnscache.Resolver
// If we were provided an address to dial, then create a resolver that just
// returns that value; otherwise, fall back to DNS.
if addr.IsValid() {
dns = &dnscache.Resolver{
SingleHostStaticResult: []netip.Addr{addr},
SingleHost: u.Hostname(),
}
} else {
dns = &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
}
}
var dialer dnscache.DialContextFunc
if a.Dialer != nil {
dialer = a.Dialer
} else {
dialer = stdDialer.DialContext
}
tr := http.DefaultTransport.(*http.Transport).Clone()
defer tr.CloseIdleConnections()
tr.Proxy = a.proxyFunc
tr.Proxy = a.getProxyFunc()
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.DialContext = dnscache.Dialer(a.dialer, dns)
tr.DialContext = dnscache.Dialer(dialer, dns)
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig)
tr.TLSClientConfig = tlsdial.Config(a.Hostname, tr.TLSClientConfig)
if a.insecureTLS {
tr.TLSClientConfig.InsecureSkipVerify = true
tr.TLSClientConfig.VerifyConnection = nil
}
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the

View File

@@ -7,27 +7,31 @@ package controlhttp
import (
"context"
"encoding/base64"
"errors"
"net"
"net/url"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/types/key"
)
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
// bi-directional communication over an HTTP connection when in JS.
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if d.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
init, cont, err := controlbase.ClientDeferred(d.MachineKey, d.ControlKey, d.ProtocolVersion)
if err != nil {
return nil, err
}
wsScheme := "wss"
host := d.Hostname
if host == "localhost" {
wsScheme = "ws"
host = net.JoinHostPort(host, httpPort)
host = net.JoinHostPort(host, strDef(d.HTTPPort, "80"))
}
wsURL := &url.URL{
Scheme: wsScheme,
@@ -52,5 +56,4 @@ func Dial(ctx context.Context, host string, httpPort string, httpsPort string, m
return nil, err
}
return cbConn, nil
}

View File

@@ -4,6 +4,17 @@
package controlhttp
import (
"net/http"
"net/url"
"time"
"tailscale.com/net/dnscache"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
@@ -18,3 +29,64 @@ const (
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)
// Dialer contains configuration on how to dial the Tailscale control server.
type Dialer struct {
// Hostname is the hostname to connect to, with no port number.
//
// This field is required.
Hostname string
// MachineKey contains the current machine's private key.
//
// This field is required.
MachineKey key.MachinePrivate
// ControlKey contains the expected public key for the control server.
//
// This field is required.
ControlKey key.MachinePublic
// ProtocolVersion is the expected protocol version to negotiate.
//
// This field is required.
ProtocolVersion uint16
// HTTPPort is the port number to use when making a HTTP connection.
//
// If not specified, this defaults to port 80.
HTTPPort string
// HTTPSPort is the port number to use when making a HTTPS connection.
//
// If not specified, this defaults to port 443.
HTTPSPort string
// Dialer is the dialer used to make outbound connections.
//
// If not specified, this defaults to net.Dialer.DialContext.
Dialer dnscache.DialContextFunc
// Logf, if set, is a logging function to use; if unset, logs are
// dropped.
Logf logger.Logf
// 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.
DialPlan *tailcfg.ControlDialPlan
proxyFunc func(*http.Request) (*url.URL, error) // or nil
// For tests only
drainFinished chan struct{}
insecureTLS bool
testFallbackDelay time.Duration
}
func strDef(v1, v2 string) string {
if v1 != "" {
return v1
}
return v2
}

View File

@@ -13,16 +13,21 @@ import (
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"runtime"
"strconv"
"sync"
"testing"
"time"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
type httpTestParam struct {
@@ -170,15 +175,16 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
defer cancel()
}
a := dialParams{
host: "localhost",
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
machineKey: client,
controlKey: server.Public(),
version: testProtocolVersion,
a := &Dialer{
Hostname: "localhost",
HTTPPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
HTTPSPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
MachineKey: client,
ControlKey: server.Public(),
ProtocolVersion: testProtocolVersion,
Dialer: new(tsdial.Dialer).SystemDial,
Logf: t.Logf,
insecureTLS: true,
dialer: new(tsdial.Dialer).SystemDial,
testFallbackDelay: 50 * time.Millisecond,
}
@@ -443,3 +449,263 @@ func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
w.(http.Flusher).Flush()
<-r.Context().Done()
}
func TestDialPlan(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("only works on Linux due to multiple localhost addresses")
}
client, server := key.NewMachine(), key.NewMachine()
const (
testProtocolVersion = 1
// We need consistent ports for each address; these are chosen
// randomly and we hope that they won't conflict during this test.
httpPort = "40080"
httpsPort = "40443"
)
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
done := make(chan struct{})
t.Cleanup(func() {
close(done)
})
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := AcceptHTTP(context.Background(), w, r, server)
if err != nil {
log.Print(err)
} else {
defer conn.Close()
}
w.Header().Set("X-Handler-Name", name)
<-done
})
if wrap != nil {
handler = wrap(handler)
}
httpLn, err := net.Listen("tcp", host.String()+":"+httpPort)
if err != nil {
t.Fatalf("HTTP listen: %v", err)
}
httpsLn, err := net.Listen("tcp", host.String()+":"+httpsPort)
if err != nil {
t.Fatalf("HTTPS listen: %v", err)
}
httpServer := &http.Server{Handler: handler}
go httpServer.Serve(httpLn)
t.Cleanup(func() {
httpServer.Close()
})
httpsServer := &http.Server{
Handler: handler,
TLSConfig: tlsConfig(t),
ErrorLog: logger.StdLogger(logger.WithPrefix(t.Logf, "http.Server.ErrorLog: ")),
}
go httpsServer.ServeTLS(httpsLn, "", "")
t.Cleanup(func() {
httpsServer.Close()
})
return
}
fallbackAddr := netip.MustParseAddr("127.0.0.1")
goodAddr := netip.MustParseAddr("127.0.0.2")
otherAddr := netip.MustParseAddr("127.0.0.3")
other2Addr := netip.MustParseAddr("127.0.0.4")
brokenAddr := netip.MustParseAddr("127.0.0.10")
testCases := []struct {
name string
plan *tailcfg.ControlDialPlan
wrap func(http.Handler) http.Handler
want netip.Addr
allowFallback bool
}{
{
name: "single",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
}},
want: goodAddr,
},
{
name: "broken-then-good",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// Dials the broken one, which fails, and then
// eventually dials the good one and succeeds
{IP: brokenAddr, Priority: 2, DialTimeoutSec: 10},
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10, DialStartDelaySec: 1},
}},
want: goodAddr,
},
{
name: "multiple-priority-fast-path",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// Dials some good IPs and our bad one (which
// hangs forever), which then hits the fast
// path where we bail without waiting.
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
}},
want: otherAddr,
},
{
name: "multiple-priority-slow-path",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// Our broken address is the highest priority,
// so we don't hit our fast path.
{IP: brokenAddr, Priority: 10, DialTimeoutSec: 10},
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
}},
want: otherAddr,
},
{
name: "fallback",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 1},
}},
want: fallbackAddr,
allowFallback: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
makeHandler(t, "fallback", fallbackAddr, nil)
makeHandler(t, "good", goodAddr, nil)
makeHandler(t, "other", otherAddr, nil)
makeHandler(t, "other2", other2Addr, nil)
makeHandler(t, "broken", brokenAddr, func(h http.Handler) http.Handler {
return http.HandlerFunc(brokenMITMHandler)
})
dialer := closeTrackDialer{
t: t,
inner: new(tsdial.Dialer).SystemDial,
conns: make(map[*closeTrackConn]bool),
}
defer dialer.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// By default, we intentionally point to something that
// we know won't connect, since we want a fallback to
// DNS to be an error.
host := "example.com"
if tt.allowFallback {
host = "localhost"
}
drained := make(chan struct{})
a := &Dialer{
Hostname: host,
HTTPPort: httpPort,
HTTPSPort: httpsPort,
MachineKey: client,
ControlKey: server.Public(),
ProtocolVersion: testProtocolVersion,
Dialer: dialer.Dial,
Logf: t.Logf,
DialPlan: tt.plan,
proxyFunc: func(*http.Request) (*url.URL, error) { return nil, nil },
drainFinished: drained,
insecureTLS: true,
testFallbackDelay: 50 * time.Millisecond,
}
conn, err := a.dial(ctx)
if err != nil {
t.Fatalf("dialing controlhttp: %v", err)
}
defer conn.Close()
raddr := conn.RemoteAddr().(*net.TCPAddr)
got, ok := netip.AddrFromSlice(raddr.IP)
if !ok {
t.Errorf("invalid remote IP: %v", raddr.IP)
} else if got != tt.want {
t.Errorf("got connection from %q; want %q", got, tt.want)
} else {
t.Logf("successfully connected to %q", raddr.String())
}
// Wait until our dialer drains so we can verify that
// all connections are closed.
<-drained
})
}
}
type closeTrackDialer struct {
t testing.TB
inner dnscache.DialContextFunc
mu sync.Mutex
conns map[*closeTrackConn]bool
}
func (d *closeTrackDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
c, err := d.inner(ctx, network, addr)
if err != nil {
return nil, err
}
ct := &closeTrackConn{Conn: c, d: d}
d.mu.Lock()
d.conns[ct] = true
d.mu.Unlock()
return ct, nil
}
func (d *closeTrackDialer) Done() {
// Unfortunately, tsdial.Dialer.SystemDial closes connections
// asynchronously in a goroutine, so we can't assume that everything is
// closed by the time we get here.
//
// Sleep/wait a few times on the assumption that things will close
// "eventually".
const iters = 100
for i := 0; i < iters; i++ {
d.mu.Lock()
if len(d.conns) == 0 {
d.mu.Unlock()
return
}
// Only error on last iteration
if i != iters-1 {
d.mu.Unlock()
time.Sleep(100 * time.Millisecond)
continue
}
for conn := range d.conns {
d.t.Errorf("expected close of conn %p; RemoteAddr=%q", conn, conn.RemoteAddr().String())
}
d.mu.Unlock()
}
}
func (d *closeTrackDialer) noteClose(c *closeTrackConn) {
d.mu.Lock()
delete(d.conns, c) // safe if already deleted
d.mu.Unlock()
}
type closeTrackConn struct {
net.Conn
d *closeTrackDialer
}
func (c *closeTrackConn) Close() error {
c.d.noteClose(c)
return c.Conn.Close()
}

View File

@@ -82,6 +82,12 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)

View File

@@ -13,20 +13,18 @@ import (
)
// disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnP atomic.Bool
var disableUPnPControl atomic.Bool
func init() {
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
}
var disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
// DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled.
func DisableUPnP() bool {
return disableUPnP.Load()
return disableUPnPControl.Load() || disableUPnpEnv()
}
// SetDisableUPnP sets whether control says that UPnP should be
// disabled.
func SetDisableUPnP(v bool) {
disableUPnP.Store(v)
disableUPnPControl.Store(v)
}

View File

@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package derp implements DERP, the Detour Encrypted Routing Protocol.
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
// protocol.
//
// DERP routes packets to clients using curve25519 keys as addresses.
//
@@ -18,7 +19,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"time"
)
@@ -194,7 +194,7 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
}
remain := frameLen - uint32(n)
if remain > 0 {
if _, err := io.CopyN(ioutil.Discard, br, int64(remain)); err != nil {
if _, err := io.CopyN(io.Discard, br, int64(remain)); err != nil {
return 0, 0, err
}
err = io.ErrShortBuffer

View File

@@ -18,7 +18,6 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"math/big"
@@ -47,8 +46,6 @@ import (
"tailscale.com/version"
)
var debug = envknob.Bool("DERP_DEBUG_LOGS")
// verboseDropKeys is the set of destination public keys that should
// verbosely log whenever DERP drops a packet.
var verboseDropKeys = map[key.NodePublic]bool{}
@@ -106,6 +103,7 @@ type Server struct {
limitedLogf logger.Logf
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
debug bool
// Counters:
packetsSent, bytesSent expvar.Int
@@ -299,6 +297,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
runtime.ReadMemStats(&ms)
s := &Server{
debug: envknob.Bool("DERP_DEBUG_LOGS"),
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
@@ -758,7 +757,7 @@ func (c *sclient) run(ctx context.Context) error {
}
func (c *sclient) handleUnknownFrame(ft frameType, fl uint32) error {
_, err := io.CopyN(ioutil.Discard, c.br, int64(fl))
_, err := io.CopyN(io.Discard, c.br, int64(fl))
return err
}
@@ -801,7 +800,7 @@ func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
return err
}
if extra := int64(fl) - int64(len(m)); extra > 0 {
_, err = io.CopyN(ioutil.Discard, c.br, extra)
_, err = io.CopyN(io.Discard, c.br, extra)
}
select {
case c.sendPongCh <- [8]byte(m):
@@ -980,7 +979,7 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
if debug {
if s.debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
}
@@ -1828,7 +1827,7 @@ func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
var bufioWriterPool = &sync.Pool{
New: func() any {
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
return bufio.NewWriterSize(io.Discard, 2<<10)
},
}
@@ -1861,7 +1860,7 @@ func (w *lazyBufioWriter) Flush() error {
}
err := w.lbw.Flush()
w.lbw.Reset(ioutil.Discard)
w.lbw.Reset(io.Discard)
bufioWriterPool.Put(w.lbw)
w.lbw = nil

View File

@@ -15,9 +15,9 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"reflect"
"sync"
"testing"
@@ -1240,7 +1240,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
}
func BenchmarkWriteUint32(b *testing.B) {
w := bufio.NewWriter(ioutil.Discard)
w := bufio.NewWriter(io.Discard)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -1279,9 +1279,9 @@ func waitConnect(t testing.TB, c *Client) {
}
func TestParseSSOutput(t *testing.T) {
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
contents, err := os.ReadFile("testdata/example_ss.txt")
if err != nil {
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
t.Errorf("os.ReadFile(example_ss.txt) failed: %v", err)
}
seen := parseSSOutput(string(contents))
if len(seen) == 0 {

View File

@@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/netip"
@@ -432,7 +431,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
return nil, 0, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
b, _ := ioutil.ReadAll(resp.Body)
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
}

View File

@@ -37,7 +37,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: AUTH_KEY
key: TS_AUTH_KEY
optional: true
- name: TS_DEST_IP
value: "{{TS_DEST_IP}}"

View File

@@ -17,10 +17,11 @@ TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}"
TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}"
TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}"
TS_SOCKET="${TS_SOCKET:-/tmp/tailscaled.sock}"
set -e
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
TAILSCALED_ARGS="--socket=${TS_SOCKET}"
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}"
@@ -81,11 +82,11 @@ if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then
fi
echo "Running tailscale up"
tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
tailscale --socket="${TS_SOCKET}" up ${UP_ARGS}
if [[ ! -z "${TS_DEST_IP}" ]]; then
echo "Adding iptables rule for DNAT"
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
iptables -t nat -I PREROUTING -d "$(tailscale --socket=${TS_SOCKET} ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
fi
echo "Waiting for tailscaled to exit"

View File

@@ -23,7 +23,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: AUTH_KEY
key: TS_AUTH_KEY
optional: true
securityContext:
capabilities:

View File

@@ -23,7 +23,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: AUTH_KEY
key: TS_AUTH_KEY
optional: true
- name: TS_ROUTES
value: "{{TS_ROUTES}}"

View File

@@ -26,5 +26,5 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: AUTH_KEY
key: TS_AUTH_KEY
optional: true

View File

@@ -17,30 +17,43 @@
package envknob
import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"tailscale.com/types/opt"
"tailscale.com/version/distro"
)
var (
mu sync.Mutex
set = map[string]string{}
list []string
mu sync.Mutex
set = map[string]string{}
regStr = map[string]*string{}
regBool = map[string]*bool{}
regOptBool = map[string]*opt.Bool{}
)
func noteEnv(k, v string) {
if v == "" {
return
}
mu.Lock()
defer mu.Unlock()
if _, ok := set[k]; !ok {
list = append(list, k)
noteEnvLocked(k, v)
}
func noteEnvLocked(k, v string) {
if v != "" {
set[k] = v
} else {
delete(set, k)
}
set[k] = v
}
// logf is logger.Logf, but logger depends on envknob, so for circular
@@ -52,11 +65,39 @@ type logf = func(format string, args ...any)
func LogCurrent(logf logf) {
mu.Lock()
defer mu.Unlock()
list := make([]string, 0, len(set))
for k := range set {
list = append(list, k)
}
sort.Strings(list)
for _, k := range list {
logf("envknob: %s=%q", k, set[k])
}
}
// Setenv changes an environment variable.
//
// It is not safe for concurrent reading of environment variables via the
// Register functions. All Setenv calls are meant to happen early in main before
// any goroutines are started.
func Setenv(envVar, val string) {
mu.Lock()
defer mu.Unlock()
os.Setenv(envVar, val)
noteEnvLocked(envVar, val)
if p := regStr[envVar]; p != nil {
*p = val
}
if p := regBool[envVar]; p != nil {
setBoolLocked(p, envVar, val)
}
if p := regOptBool[envVar]; p != nil {
setOptBoolLocked(p, envVar, val)
}
}
// String returns the named environment variable, using os.Getenv.
//
// If the variable is non-empty, it's also tracked & logged as being
@@ -67,6 +108,82 @@ func String(envVar string) string {
return v
}
// RegisterString returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterString(envVar string) func() string {
mu.Lock()
defer mu.Unlock()
p, ok := regStr[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = &val
regStr[envVar] = p
}
return func() string { return *p }
}
// RegisterBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterBool(envVar string) func() bool {
mu.Lock()
defer mu.Unlock()
p, ok := regBool[envVar]
if !ok {
var b bool
p = &b
setBoolLocked(p, envVar, os.Getenv(envVar))
regBool[envVar] = p
}
return func() bool { return *p }
}
// RegisterOptBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterOptBool(envVar string) func() opt.Bool {
mu.Lock()
defer mu.Unlock()
p, ok := regOptBool[envVar]
if !ok {
var b opt.Bool
p = &b
setOptBoolLocked(p, envVar, os.Getenv(envVar))
regOptBool[envVar] = p
}
return func() opt.Bool { return *p }
}
func setBoolLocked(p *bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = false
return
}
var err error
*p, err = strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
}
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = ""
return
}
b, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
p.Set(b)
}
// Bool returns the boolean value of the named environment variable.
// If the variable is not set, it returns false.
// An invalid value exits the binary with a failure.
@@ -81,6 +198,7 @@ func BoolDefaultTrue(envVar string) bool {
}
func boolOr(envVar string, implicitValue bool) bool {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return implicitValue
@@ -98,6 +216,7 @@ func boolOr(envVar string, implicitValue bool) bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupBool(envVar string) (v bool, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return false, false
@@ -113,6 +232,7 @@ func LookupBool(envVar string) (v bool, ok bool) {
// OptBool is like Bool, but returns an opt.Bool, so the caller can
// distinguish between implicitly and explicitly false.
func OptBool(envVar string) opt.Bool {
assertNotInInit()
b, ok := LookupBool(envVar)
if !ok {
return ""
@@ -126,6 +246,7 @@ func OptBool(envVar string) opt.Bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupInt(envVar string) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
@@ -164,5 +285,142 @@ func NoLogsNoSupport() bool {
// SetNoLogsNoSupport enables no-logs-no-support mode.
func SetNoLogsNoSupport() {
os.Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
}
// notInInit is set true the first time we've seen a non-init stack trace.
var notInInit atomic.Bool
func assertNotInInit() {
if notInInit.Load() {
return
}
skip := 0
for {
pc, _, _, ok := runtime.Caller(skip)
if !ok {
notInInit.Store(true)
return
}
fu := runtime.FuncForPC(pc)
if fu == nil {
return
}
name := fu.Name()
name = strings.TrimRightFunc(name, func(r rune) bool { return r >= '0' && r <= '9' })
if strings.HasSuffix(name, ".init") || strings.HasSuffix(name, ".init.") {
stack := make([]byte, 1<<10)
stack = stack[:runtime.Stack(stack, false)]
envCheckedInInitStack = stack
}
skip++
}
}
var envCheckedInInitStack []byte
// PanicIfAnyEnvCheckedInInit panics if environment variables were read during
// init.
func PanicIfAnyEnvCheckedInInit() {
if envCheckedInInitStack != nil {
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
}
}
var applyDiskConfigErr error
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
func ApplyDiskConfigError() error { return applyDiskConfigErr }
// ApplyDiskConfig returns a platform-specific config file of environment keys/values and
// applies them. On Linux and Unix operating systems, it's a no-op and always returns nil.
// If no platform-specific config file is found, it also returns nil.
//
// It exists primarily for Windows to make it easy to apply environment variables to
// a running service in a way similar to modifying /etc/default/tailscaled on Linux.
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
func ApplyDiskConfig() (err error) {
var f *os.File
defer func() {
if err != nil {
// Stash away our return error for the healthcheck package to use.
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
}
}()
// First try the explicitly-provided value for development testing. Not
// useful for users to use on their own. (if they can set this, they can set
// any environment variable anyway)
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
f, err = os.Open(name)
if err != nil {
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
}
defer f.Close()
return applyKeyValueEnv(f)
}
name := getPlatformEnvFile()
if name == "" {
return nil
}
f, err = os.Open(name)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
defer f.Close()
return applyKeyValueEnv(f)
}
// getPlatformEnvFile returns the current platform's path to an optional
// tailscaled-env.txt file. It returns an empty string if none is defined
// for the platform.
func getPlatformEnvFile() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
case "linux":
if distro.Get() == distro.Synology {
return "/etc/tailscale/tailscaled-env.txt"
}
case "darwin":
// TODO(bradfitz): figure this out. There are three ways to run
// Tailscale on macOS (tailscaled, GUI App Store, GUI System Extension)
// and we should deal with all three.
}
return ""
}
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
//
// Empty lines and lines beginning with '#' are skipped.
//
// Values can be double quoted, in which case they're unquoted using
// strconv.Unquote.
func applyKeyValueEnv(r io.Reader) error {
bs := bufio.NewScanner(r)
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
k = strings.TrimSpace(k)
if !ok || k == "" {
continue
}
v = strings.TrimSpace(v)
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return fmt.Errorf("invalid value in line %q: %v", line, err)
}
}
Setenv(k, v)
}
return bs.Err()
}

View File

@@ -325,7 +325,7 @@ func OverallError() error {
return overallErrorLocked()
}
var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
func overallErrorLocked() error {
if !anyInterfaceUp {
@@ -383,7 +383,10 @@ func overallErrorLocked() error {
for _, s := range controlHealth {
errs = append(errs, errors.New(s))
}
if e := fakeErrForTesting; len(errs) == 0 && e != "" {
if err := envknob.ApplyDiskConfigError(); err != nil {
errs = append(errs, err)
}
if e := fakeErrForTesting(); len(errs) == 0 && e != "" {
return errors.New(e)
}
sort.Slice(errs, func(i, j int) bool {

View File

@@ -9,7 +9,6 @@ package hostinfo
import (
"bytes"
"io/ioutil"
"os"
"strings"
@@ -99,11 +98,11 @@ func linuxVersionMeta() (meta versionMeta) {
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
case distro.WDMyCloud:
slurp, _ := ioutil.ReadFile("/etc/version")
slurp, _ := os.ReadFile("/etc/version")
meta.DistroVersion = string(bytes.TrimSpace(slurp))
return
case distro.QNAP:
slurp, _ := ioutil.ReadFile("/etc/version_info")
slurp, _ := os.ReadFile("/etc/version_info")
meta.DistroVersion = getQnapQtsVersion(string(slurp))
return
}
@@ -133,7 +132,7 @@ func linuxVersionMeta() (meta versionMeta) {
case "debian":
// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
// Or "bookworm/sid" on sid/testing.
slurp, _ := ioutil.ReadFile("/etc/debian_version")
slurp, _ := os.ReadFile("/etc/debian_version")
if v := string(bytes.TrimSpace(slurp)); v != "" {
if '0' <= v[0] && v[0] <= '9' {
meta.DistroVersion = v
@@ -143,7 +142,7 @@ func linuxVersionMeta() (meta versionMeta) {
}
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
if meta.DistroVersion == "" {
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
meta.DistroVersion = string(bytes.TrimSpace(cr))
}
}

View File

@@ -5,8 +5,11 @@
package ipnlocal
import (
"encoding/json"
"io"
"net/http"
"tailscale.com/tailcfg"
)
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
@@ -15,6 +18,21 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
// Test handler.
body, _ := io.ReadAll(r.Body)
w.Write(body)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
res, err := b.getSSHUsernames(&req)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}

View File

@@ -24,7 +24,6 @@ import (
"time"
"go4.org/netipx"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
@@ -68,7 +67,6 @@ import (
)
var controlDebugFlags = getControlDebugFlags()
var canSSH = envknob.CanSSHD()
func getControlDebugFlags() []string {
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
@@ -191,6 +189,10 @@ type LocalBackend struct {
// statusChanged.Broadcast().
statusLock sync.Mutex
statusChanged *sync.Cond
// dialPlan is any dial plan that we've received from the control
// server during a previous connection; it is cleared on logout.
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
}
// clientGen is a func that creates a control plane client.
@@ -575,6 +577,10 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
b.mu.Lock()
defer b.mu.Unlock()
return b.peerCapsLocked(src)
}
func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
if b.netMap == nil {
return nil
}
@@ -586,9 +592,9 @@ func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
if !a.IsSingleIP() {
continue
}
dstIP := a.Addr()
if dstIP.BitLen() == src.BitLen() {
return filt.AppendCaps(nil, src, a.Addr())
dst := a.Addr()
if dst.BitLen() == src.BitLen() { // match on family
return filt.AppendCaps(nil, src, dst)
}
}
return nil
@@ -682,6 +688,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
}
if st.NetMap != nil {
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
b.logf("[v1] TKA sync error: %v", err)
}
if b.findExitNodeIDLocked(st.NetMap) {
prefsChanged = true
}
@@ -1082,6 +1091,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
Dialer: b.Dialer(),
Status: b.setClientStatus,
C2NHandler: http.HandlerFunc(b.handleC2N),
DialPlan: &b.dialPlan, // pointer because it can't be copied
// Don't warn about broken Linux IP forwarding when
// netstack is being used.
@@ -1510,12 +1520,12 @@ func (b *LocalBackend) tellClientToBrowseToURL(url string) {
}
// For testing lazy machine key generation.
var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
var cache syncs.AtomicValue[key.MachinePrivate]
return func() (key.MachinePrivate, error) {
if panicOnMachineKeyGeneration {
if panicOnMachineKeyGeneration() {
panic("machine key generated")
}
if v, ok := cache.LoadOk(); ok {
@@ -1752,7 +1762,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
// from the prefs p, which may be nil.
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
b.sshAtomicBool.Store(p != nil && p.RunSSH && canSSH)
b.sshAtomicBool.Store(p != nil && p.RunSSH && envknob.CanSSHD())
if p == nil {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
@@ -1967,7 +1977,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !canSSH {
if !envknob.CanSSHD() {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
@@ -2032,7 +2042,7 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
b.logf("EditPrefs check error: %v", err)
return nil, err
}
if p1.RunSSH && !canSSH {
if p1.RunSSH && !envknob.CanSSHD() {
b.mu.Unlock()
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
return nil, errors.New("Tailscale SSH server administratively disabled.")
@@ -2854,7 +2864,7 @@ func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Pre
hi.ShieldsUp = prefs.ShieldsUp
var sshHostKeys []string
if prefs.RunSSH && canSSH {
if prefs.RunSSH && envknob.CanSSHD() {
// TODO(bradfitz): this is called with b.mu held. Not ideal.
// If the filesystem gets wedged or something we could block for
// a long time. But probably fine.
@@ -3073,7 +3083,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.setAtomicValuesFromPrefs(nil)
}
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && canSSH }
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
@@ -3107,6 +3117,9 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
})
// Clear any previous dial plan(s), if set.
b.dialPlan.Store(nil)
if cc == nil {
// Double Logout can happen via repeated IPN
// connections to ipnserver making it repeatedly
@@ -3223,6 +3236,17 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
}
// operatorUserName returns the current pref's OperatorUser's name, or the
// empty string if none.
func (b *LocalBackend) operatorUserName() string {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return ""
}
return b.prefs.OperatorUser
}
// OperatorUserID returns the current pref's OperatorUser's ID (in
// os/user.User.Uid string form), or the empty string if none.
func (b *LocalBackend) OperatorUserID() string {
@@ -3305,13 +3329,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
if p.User != nm.User && !slices.Contains(p.Capabilities, tailcfg.CapabilityFileSharingTarget) {
if len(p.Addresses) == 0 {
continue
}
if p.User != nm.User && b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharing) {
continue
}
peerAPI := peerAPIBase(b.netMap, p)
if peerAPI == "" {
continue
}
ret = append(ret, &apitype.FileTarget{
Node: p,
@@ -3322,6 +3348,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return ret, nil
}
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap string) bool {
for _, hasCap := range b.peerCapsLocked(addr) {
if hasCap == wantCap {
return true
}
}
return false
}
// SetDNS adds a DNS record for the given domain name & TXT record
// value.
//
@@ -3580,6 +3615,17 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
return cc.DoNoiseRequest(req)
}
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
// on prefs. It returns false if there are no prefs set.
func (b *LocalBackend) tailscaleSSHEnabled() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
return b.prefs.RunSSH
}
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
b.mu.Lock()
defer b.mu.Unlock()

View File

@@ -478,8 +478,8 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = true
defer func(old func() bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = func() bool { return true }
var logf logger.Logf = logger.Discard
store := new(mem.Store)

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@@ -12,6 +12,8 @@ import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"tailscale.com/envknob"
@@ -24,13 +26,125 @@ import (
"tailscale.com/types/tkatype"
)
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
var networkLockAvailable = envknob.RegisterBool("TS_EXPERIMENTAL_NETWORK_LOCK")
type tkaState struct {
authority *tka.Authority
storage *tka.FS
}
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
// performing the steps necessary to synchronize local tka state.
//
// There are 4 scenarios handled here:
// - Enablement: nm.TKAEnabled but b.tka == nil
// ∴ reach out to /machine/tka/boostrap to get the genesis AUM, then
// initialize TKA.
// - Disablement: !nm.TKAEnabled but b.tka != nil
// ∴ reach out to /machine/tka/boostrap to read the disablement secret,
// then verify and clear tka local state.
// - Sync needed: b.tka.Head != nm.TKAHead
// ∴ complete multi-step synchronization flow.
// - Everything up to date: All other cases.
// ∴ no action necessary.
//
// b.mu must be held. b.mu will be stepped out of (and back in) during network
// RPCs.
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
if !networkLockAvailable() {
// If the feature flag is not enabled, pretend we don't exist.
return nil
}
if nm.SelfNode == nil {
return errors.New("SelfNode missing")
}
isEnabled := b.tka != nil
wantEnabled := nm.TKAEnabled
if isEnabled != wantEnabled {
var ourHead tka.AUMHash
if b.tka != nil {
ourHead = b.tka.authority.Head()
}
// Regardless of whether we are moving to disabled or enabled, we
// need information from the tka bootstrap endpoint.
b.mu.Unlock()
bs, err := b.tkaFetchBootstrap(nm.SelfNode.ID, ourHead)
b.mu.Lock()
if err != nil {
return fmt.Errorf("fetching bootstrap: %v", err)
}
if wantEnabled && !isEnabled {
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
return fmt.Errorf("bootstrap: %v", err)
}
isEnabled = true
} else if !wantEnabled && isEnabled {
if b.tka.authority.ValidDisablement(bs.DisablementSecret) {
b.tka = nil
isEnabled = false
if err := os.RemoveAll(b.chonkPath()); err != nil {
return fmt.Errorf("os.RemoveAll: %v", err)
}
} else {
b.logf("Disablement secret did not verify, leaving TKA enabled.")
}
} else {
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
}
}
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
// TODO(tom): Implement sync
}
return nil
}
// chonkPath returns the absolute path to the directory in which TKA
// state (the 'tailchonk') is stored.
func (b *LocalBackend) chonkPath() string {
return filepath.Join(b.TailscaleVarRoot(), "tka")
}
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
// tailnet key authority, based on the given genesis AUM.
//
// b.mu must be held.
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
if !b.CanSupportNetworkLock() {
return errors.New("network lock not supported in this configuration")
}
var genesis tka.AUM
if err := genesis.Unserialize(g); err != nil {
return fmt.Errorf("reading genesis: %v", err)
}
chonkDir := b.chonkPath()
if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("mkdir: %v", err)
}
chonk, err := tka.ChonkDir(chonkDir)
if err != nil {
return fmt.Errorf("chonk: %v", err)
}
authority, err := tka.Bootstrap(chonk, genesis)
if err != nil {
return fmt.Errorf("tka bootstrap: %v", err)
}
b.tka = &tkaState{
authority: authority,
storage: chonk,
}
return nil
}
// CanSupportNetworkLock returns true if tailscaled is able to operate
// a local tailnet key authority (and hence enforce network lock).
func (b *LocalBackend) CanSupportNetworkLock() bool {
@@ -82,7 +196,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
if b.tka != nil {
return errors.New("network-lock is already initialized")
}
if !networkLockAvailable {
if !networkLockAvailable() {
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
}
if !b.CanSupportNetworkLock() {
@@ -237,3 +351,50 @@ func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.Node
return a, nil
}
}
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
// over noise. This is used to get values necessary to enable or disable TKA.
func (b *LocalBackend) tkaFetchBootstrap(nodeID tailcfg.NodeID, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
bootstrapReq := tailcfg.TKABootstrapRequest{
NodeID: nodeID,
}
if !head.IsZero() {
head, err := head.MarshalText()
if err != nil {
return nil, fmt.Errorf("head.MarshalText failed: %v", err)
}
bootstrapReq.Head = string(head)
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(bootstrapReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/bootstrap", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKABootstrapResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}

View File

@@ -0,0 +1,243 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
import (
"bytes"
"context"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"tailscale.com/control/controlclient"
"tailscale.com/hostinfo"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
)
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
hi := hostinfo.New()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
k := key.NewMachine()
opts := controlclient.Options{
ServerURL: "https://example.com",
Hostinfo: hi,
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
},
HTTPTestClient: c,
NoiseTestClient: c,
Status: func(controlclient.Status) {},
}
cc, err := controlclient.NewNoStart(opts)
if err != nil {
t.Fatal(err)
}
return cc
}
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
// httptest plumbing, despite the domain being unused in the actual noise request transport.
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
ts := httptest.NewUnstartedServer(handler)
ts.StartTLS()
client := ts.Client()
client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, ts.Listener.Addr().String())
}
return ts, client
}
func TestTKAEnablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
// Make a fake TKA authority, getting a usable genesis AUM which
// our mock server can communicate.
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
a1, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/bootstrap":
body := new(tailcfg.TKABootstrapRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.NodeID != 420 {
t.Errorf("bootstrap nodeID=%v, want 420", body.NodeID)
}
if body.Head != "" {
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
}
w.WriteHeader(200)
out := tailcfg.TKABootstrapResponse{
GenesisAUM: genesisAUM.Serialize(),
}
if err := json.NewEncoder(w).Encode(out); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
temp := t.TempDir()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
}
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 420},
TKAEnabled: true,
TKAHead: tka.AUMHash{},
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka == nil {
t.Fatal("tka was not initialized")
}
if b.tka.authority.Head() != a1.Head() {
t.Errorf("authority.Head() = %x, want %x", b.tka.authority.Head(), a1.Head())
}
}
func TestTKADisablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/bootstrap":
body := new(tailcfg.TKABootstrapRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
var disablement []byte
switch body.NodeID {
case 42:
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
case 420:
disablement = disablementSecret
default:
t.Errorf("bootstrap nodeID=%v, wanted 42 or 420", body.NodeID)
}
var head tka.AUMHash
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
t.Fatalf("failed unmarshal of body.Head: %v", err)
}
if head != authority.Head() {
t.Errorf("reported head = %x, want %x", head, authority.Head())
}
w.WriteHeader(200)
out := tailcfg.TKABootstrapResponse{
DisablementSecret: disablement,
}
if err := json.NewEncoder(w).Encode(out); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
}
// Test that the wrong disablement secret does not shut down the authority.
// NodeID == 42 indicates this scenario to our mock server.
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 42},
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka == nil {
t.Error("TKA was disabled despite incorrect disablement secret")
}
// Test the correct disablement secret shuts down the authority.
// NodeID == 420 indicates this scenario to our mock server.
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 420},
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka != nil {
t.Fatal("tka was not shut down")
}
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
}
}

View File

@@ -44,6 +44,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/strs"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
@@ -720,8 +721,8 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
return
}
rawPath := r.URL.EscapedPath()
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
if suffix == rawPath {
suffix, ok := strs.CutPrefix(rawPath, "/v0/put/")
if !ok {
http.Error(w, "misconfigured internals", 500)
return
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
@@ -87,7 +86,7 @@ func fileHasContents(name string, want string) check {
return
}
path := filepath.Join(root, name)
got, err := ioutil.ReadFile(path)
got, err := os.ReadFile(path)
if err != nil {
t.Errorf("fileHasContents: %v", err)
return
@@ -517,7 +516,7 @@ func TestDeletedMarkers(t *testing.T) {
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := ioutil.ReadDir(dir); err != nil {
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {

View File

@@ -18,24 +18,98 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/envknob"
"go4.org/mem"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/util/mak"
)
var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS")
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys or try to generate for ourselves when not
// running as root.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
// getSSHUsernames discovers and returns the list of usernames that are
// potential Tailscale SSH user targets.
//
// Invariant: must not be called with b.mu held.
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
res := new(tailcfg.C2NSSHUsernamesResponse)
if !b.tailscaleSSHEnabled() {
return res, nil
}
max := 10
if req != nil && req.Max != 0 {
max = req.Max
}
add := func(u string) {
if req != nil && req.Exclude[u] {
return
}
switch u {
case "nobody", "daemon", "sync":
return
}
if slices.Contains(res.Usernames, u) {
return
}
if len(res.Usernames) > max {
// Enough for a hint.
return
}
res.Usernames = append(res.Usernames, u)
}
if opUser := b.operatorUserName(); opUser != "" {
add(opUser)
}
// Check popular usernames and see if they exist with a real shell.
switch runtime.GOOS {
case "darwin":
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
}
add(string(line))
return nil
})
default:
lineread.File("/etc/passwd", func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
return res, nil
}
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
var existing map[string]ssh.Signer
if os.Geteuid() == 0 {
@@ -83,7 +157,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
defer keyGenMu.Unlock()
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
v, err := os.ReadFile(path)
if err == nil {
return v, nil
}
@@ -124,7 +198,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
for _, typ := range keyTypes {
filename := "/etc/ssh/ssh_host_" + typ + "_key"
hostKey, err := ioutil.ReadFile(filename)
hostKey, err := os.ReadFile(filename)
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
continue
}

View File

@@ -6,6 +6,16 @@
package ipnlocal
import (
"errors"
"tailscale.com/tailcfg"
)
func (b *LocalBackend) getSSHHostKeyPublicStrings() []string {
return nil
}
func (b *LocalBackend) getSSHUsernames(*tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
return nil, errors.New("not implemented")
}

View File

@@ -2,14 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// +build linux
//go:build linux || (darwin && !ios)
// +build linux darwin,!ios
package ipnlocal
import (
"encoding/json"
"reflect"
"testing"
"tailscale.com/tailcfg"
"tailscale.com/util/must"
)
func TestSSHKeyGen(t *testing.T) {
@@ -40,3 +44,17 @@ func TestSSHKeyGen(t *testing.T) {
t.Errorf("got different keys on second call")
}
}
type fakeSSHServer struct {
SSHServer
}
func TestGetSSHUsernames(t *testing.T) {
b := new(LocalBackend)
b.sshServer = fakeSSHServer{}
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", must.Get(json.Marshal(res)))
}

View File

@@ -12,7 +12,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -215,7 +214,7 @@ func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
s.logf("blocking client while server in use; connIdentity=%v", ci)
connDone := make(chan struct{})
go func() {
io.Copy(ioutil.Discard, conn)
io.Copy(io.Discard, conn)
close(connDone)
}()
ch := make(chan struct{}, 1)
@@ -773,7 +772,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
})
if root := b.TailscaleVarRoot(); root != "" {
chonkDir := filepath.Join(root, "chonk")
chonkDir := filepath.Join(root, "tka")
if _, err := os.Stat(chonkDir); err == nil {
// The directory exists, which means network-lock has been initialized.
storage, err := tka.ChonkDir(chonkDir)
@@ -933,14 +932,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
startTime := time.Now()
log.Printf("exec: %#v %v", executable, args)
cmd := exec.Command(executable, args...)
if runtime.GOOS == "windows" {
extraEnv, err := loadExtraEnv()
if err != nil {
logf("errors loading extra env file; ignoring: %v", err)
} else {
cmd.Env = append(os.Environ(), extraEnv...)
}
}
// Create a pipe object to use as the subproc's stdin.
// When the writer goes away, the reader gets EOF.
@@ -1175,7 +1166,7 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := ioutil.ReadDir("/mnt")
fis, err := os.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
@@ -1209,38 +1200,3 @@ func findQnapTaildropDir(name string) (string, error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
func loadExtraEnv() (env []string, err error) {
if runtime.GOOS != "windows" {
return nil, nil
}
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
contents, err := os.ReadFile(name)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
for _, line := range strings.Split(string(contents), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok || k == "" {
continue
}
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
}
env = append(env, k+"="+v)
} else {
env = append(env, line)
}
}
return env, nil
}

View File

@@ -23,7 +23,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -38,6 +37,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/logger"
"tailscale.com/util/strs"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -73,7 +73,7 @@ func (h *Handler) certDir() (string, error) {
return full, nil
}
var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite && !h.PermitCert {
@@ -87,16 +87,19 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
return
}
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
if domain == r.URL.Path {
domain, ok := strs.CutPrefix(r.URL.Path, "/localapi/v0/cert/")
if !ok {
http.Error(w, "internal handler config wired wrong", 500)
return
}
if !validLookingCertDomain(domain) {
http.Error(w, "invalid domain", 400)
return
}
now := time.Now()
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
traceACME := func(v any) {
if !acmeDebug {
if !acmeDebug() {
return
}
j, _ := json.MarshalIndent(v, "", "\t")
@@ -165,6 +168,11 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
// keypair for domain exists on disk in dir that is valid at the
// provided now time.
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
if !validLookingCertDomain(domain) {
// Before we read files from disk using it, validate it's halfway
// reasonable looking.
return nil, false
}
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
certPEM, _ := os.ReadFile(certFile(dir, domain))
if validCertPEM(domain, keyPEM, certPEM, now) {
@@ -293,7 +301,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
if err := os.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
return nil, err
}
@@ -316,7 +324,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
return nil, err
}
}
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
if err := os.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
return nil, err
}
@@ -372,7 +380,7 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := ioutil.ReadFile(pemName); err == nil {
if v, err := os.ReadFile(pemName); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
@@ -388,7 +396,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
return nil, err
}
return privKey, nil
@@ -426,6 +434,21 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
return err == nil
}
// validLookingCertDomain reports whether name looks like a valid domain name that
// we might be able to get a cert for.
//
// It's a light check primarily for double checking before it's used
// as part of a filesystem path. The actual validation happens in checkCertDomain.
func validLookingCertDomain(name string) bool {
if name == "" ||
strings.Contains(name, "..") ||
strings.ContainsAny(name, ":/\\\x00") ||
!strings.Contains(name, ".") {
return false
}
return true
}
func checkCertDomain(st *ipnstate.Status, domain string) error {
if domain == "" {
return errors.New("missing domain name")

30
ipn/localapi/cert_test.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios && !android && !js
// +build !ios,!android,!js
package localapi
import "testing"
func TestValidLookingCertDomain(t *testing.T) {
tests := []struct {
in string
want bool
}{
{"foo.com", true},
{"foo..com", false},
{"foo/com.com", false},
{"NUL", false},
{"", false},
{"foo\\bar.com", false},
{"foo\x00bar.com", false},
}
for _, tt := range tests {
if got := validLookingCertDomain(tt.in); got != tt.want {
t.Errorf("validLookingCertDomain(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}

View File

@@ -9,7 +9,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/netip"
"os"
@@ -618,7 +617,7 @@ func PrefsFromBytes(b []byte) (*Prefs, error) {
// LoadPrefs loads a legacy relaynode config file into Prefs
// with sensible migration defaults set.
func LoadPrefs(filename string) (*Prefs, error) {
data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
}

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/netip"
"os"
"reflect"
@@ -474,7 +473,7 @@ func TestLoadPrefsNotExist(t *testing.T) {
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
if err != nil {
t.Fatal(err)
}

View File

@@ -9,7 +9,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -128,7 +127,7 @@ func NewFileStore(logf logger.Logf, path string) (ipn.StateStore, error) {
return nil, fmt.Errorf("creating state directory: %w", err)
}
bs, err := ioutil.ReadFile(path)
bs, err := os.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)

View File

@@ -1,47 +0,0 @@
# Tailscale for Windows dependencies
The following open source dependencies are used to build the [Tailscale client
for windows][]. See also the dependencies in the [Tailscale CLI][].
[Tailscale client for windows]: https://tailscale.com/kb/1022/install-windows/
[Tailscale CLI]: ./tailscale.md
## Go Packages
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/v1.0.0/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.5/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.5/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/lxn/walk](https://pkg.go.dev/github.com/lxn/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/ed127cfb919a/LICENSE))
- [github.com/lxn/win](https://pkg.go.dev/github.com/lxn/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/c3f813abca9f/LICENSE))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.6.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.4.10))
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/f81723ceac3f/LICENSE))
## Additional Dependencies
- [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/) ([zlib/libpng](https://nsis.sourceforge.io/License))
- [Wintun](https://www.wintun.net/) ([Prebuilt Binaries License](https://git.zx2c4.com/wintun/tree/prebuilt-binaries-license.txt))
- [wireguard-windows](https://git.zx2c4.com/wireguard-windows/) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING))

View File

@@ -9,7 +9,6 @@ package filelogger
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -186,12 +185,18 @@ func (w *logFileWriter) startNewFileLocked() {
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
fis, _ := ioutil.ReadDir(w.dir)
entries, _ := os.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, fi := range fis {
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
w.wrappedLogf("error getting log file info: %v", err)
continue
}
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue

View File

@@ -16,7 +16,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -248,7 +247,7 @@ func logsDir(logf logger.Logf) string {
// No idea where to put stuff. Try to create a temp dir. It'll
// mean we might lose some logs and rotate through log IDs, but
// it's something.
tmp, err := ioutil.TempDir("", "tailscaled-log-*")
tmp, err := os.MkdirTemp("", "tailscaled-log-*")
if err != nil {
panic("no safe place found to store log state")
}
@@ -259,7 +258,7 @@ func logsDir(logf logger.Logf) string {
// runningUnderSystemd reports whether we're running under systemd.
func runningUnderSystemd() bool {
if runtime.GOOS == "linux" && os.Getppid() == 1 {
slurp, _ := ioutil.ReadFile("/proc/1/stat")
slurp, _ := os.ReadFile("/proc/1/stat")
return bytes.HasPrefix(slurp, []byte("1 (systemd) "))
}
return false

View File

@@ -6,7 +6,7 @@ package main
import (
"flag"
"io/ioutil"
"io"
"log"
"net/http"
"net/url"
@@ -39,7 +39,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("logadopt: response read failed %d: %v", resp.StatusCode, err)

View File

@@ -9,7 +9,7 @@ import (
"bufio"
"encoding/json"
"flag"
"io/ioutil"
"io"
"log"
"net/http"
"os"
@@ -50,7 +50,7 @@ func main() {
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
}

View File

@@ -6,7 +6,7 @@ package filch
import (
"fmt"
"io/ioutil"
"io"
"os"
"runtime"
"strings"
@@ -195,7 +195,7 @@ func TestFilchStderr(t *testing.T) {
f.close(t)
pipeW.Close()
b, err := ioutil.ReadAll(pipeR)
b, err := io.ReadAll(pipeR)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,7 +13,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
@@ -430,7 +429,7 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
if resp.StatusCode != 200 {
uploaded = resp.StatusCode == 400 // the server saved the logs anyway
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
return uploaded, fmt.Errorf("log upload of %d bytes %s failed %d: %q", len(body), compressedNote, resp.StatusCode, b)
}
@@ -654,7 +653,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
return 0, nil
}
level, buf := parseAndRemoveLogLevel(buf)
if l.stderr != nil && l.stderr != ioutil.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if l.stderr != nil && l.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {

View File

@@ -9,7 +9,6 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
@@ -52,7 +51,7 @@ func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
ts.srv = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error("failed to read HTTP request")
}

View File

@@ -12,7 +12,6 @@ import (
"bytes"
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -158,7 +157,7 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
if sc.Text() == resolvconfConfigName {
continue
}
bs, err := ioutil.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
if err != nil {
if os.IsNotExist(err) {
// Probably raced with a deletion, that's okay.

View File

@@ -12,7 +12,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"net/netip"
"os"
"os/exec"
@@ -452,7 +451,7 @@ func (fs directFS) Rename(oldName, newName string) error {
func (fs directFS) Remove(name string) error { return os.Remove(fs.path(name)) }
func (fs directFS) ReadFile(name string) ([]byte, error) {
return ioutil.ReadFile(fs.path(name))
return os.ReadFile(fs.path(name))
}
func (fs directFS) Truncate(name string) error {
@@ -460,7 +459,7 @@ func (fs directFS) Truncate(name string) error {
}
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
return ioutil.WriteFile(fs.path(name), contents, perm)
return os.WriteFile(fs.path(name), contents, perm)
}
// runningAsGUIDesktopUser reports whether it seems that this code is

View File

@@ -6,14 +6,13 @@ package dns
import (
"fmt"
"io/ioutil"
"os"
"tailscale.com/types/logger"
)
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
bs, err := ioutil.ReadFile("/etc/resolv.conf")
bs, err := os.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
return newDirectManager(logf), nil
}

View File

@@ -32,7 +32,7 @@ const (
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
)
var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
var configureWSL = envknob.RegisterBool("TS_DEBUG_CONFIGURE_WSL")
type windowsManager struct {
logf logger.Logf
@@ -359,7 +359,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
// On initial setup of WSL, the restart caused by --shutdown is slow,
// so we do it out-of-line.
if configureWSL {
if configureWSL() {
go func() {
if err := m.wslManager.SetDNS(cfg); err != nil {
m.logf("WSL SetDNS: %v", err) // continue

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
@@ -474,7 +473,7 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
res, err := ioutil.ReadAll(hres.Body)
res, err := io.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
@@ -484,13 +483,13 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
return res, err
}
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
var verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if verboseDNSForward {
if verboseDNSForward() {
f.logf("forwarder.send(%q) ...", rr.name.Addr)
defer func() {
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)

View File

@@ -141,7 +141,7 @@ func (r *Resolver) ttl() time.Duration {
return 10 * time.Minute
}
var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE")
// LookupIP returns the host's primary IP address (either IPv4 or
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
@@ -167,14 +167,14 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
if debug {
if debug() {
log.Printf("dnscache: %q is an IP", host)
}
return ip, zaddr, []netip.Addr{ip}, nil
}
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q = %v (cached)", host, ip)
}
return ip, ip6, allIPs, nil
@@ -192,13 +192,13 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
if res.Err != nil {
if r.UseLastGood {
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q using %v after error", host, ip)
}
return ip, ip6, allIPs, nil
}
}
if debug {
if debug() {
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
}
return zaddr, zaddr, nil, res.Err
@@ -206,7 +206,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
r := res.Val
return r.ip, r.ip6, r.allIPs, nil
case <-ctx.Done():
if debug {
if debug() {
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
}
return zaddr, zaddr, nil, ctx.Err()
@@ -250,7 +250,7 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) {
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q found in cache as %v", host, ip)
}
return ip, ip6, allIPs, nil
@@ -300,13 +300,13 @@ func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Ad
if ip.IsPrivate() {
// Don't cache obviously wrong entries from captive portals.
// TODO: use DoH or DoT for the forwarding resolver?
if debug {
if debug() {
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
}
return
}
if debug {
if debug() {
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
}
@@ -382,7 +382,7 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC
}
i4s := v4addrs(allIPs)
if len(i4s) < 2 {
if debug {
if debug() {
log.Printf("dnscache: dialing %s, %s for %s", network, ip, address)
}
c, err := dc.dialOne(ctx, ip.Unmap())
@@ -406,7 +406,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// Can't try bootstrap DNS if we don't have a fallback function
if d.dnsCache.LookupIPFallback == nil {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: no fallback")
}
return false
@@ -415,7 +415,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// We can't retry if the context is canceled, since any further
// operations with this context will fail.
if ctxErr := ctx.Err(); ctxErr != nil {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: context error: %v", ctxErr)
}
return false
@@ -423,7 +423,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
wasTrustworthy := dc.dnsWasTrustworthy()
if wasTrustworthy {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: DNS was trustworthy")
}
return false

View File

@@ -167,10 +167,8 @@ func TestInterleaveSlices(t *testing.T) {
func TestShouldTryBootstrap(t *testing.T) {
oldDebug := debug
t.Cleanup(func() {
debug = oldDebug
})
debug = true
t.Cleanup(func() { debug = oldDebug })
debug = func() bool { return true }
type step struct {
ip netip.Addr // IP we pretended to dial

View File

@@ -10,7 +10,6 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
@@ -42,7 +41,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("dns-fallback-servers.json", out, 0644); err != nil {
if err := os.WriteFile("dns-fallback-servers.json", out, 0644); err != nil {
log.Fatal(err)
}
}

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"testing"
@@ -23,7 +22,7 @@ func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
err := os.WriteFile(procNetRoutePath, buf, 0644)
if err != nil {
t.Fatal(err)
}
@@ -87,7 +86,7 @@ func TestAwsAppRunnerDefaultRouteInterface(t *testing.T) {
"ecs-eth0\t02AAFEA9\t01ACFEA9\t0007\t0\t0\t0\tFFFFFFFF\t0\t0\t0\n" +
"ecs-eth0\t00ACFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n" +
"eth0\t00AFFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n")
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
err := os.WriteFile(procNetRoutePath, buf, 0644)
if err != nil {
t.Fatal(err)
}

View File

@@ -12,8 +12,8 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/netip"
@@ -43,7 +43,7 @@ import (
// Debugging and experimentation tweakables.
var (
debugNetcheck = envknob.Bool("TS_DEBUG_NETCHECK")
debugNetcheck = envknob.RegisterBool("TS_DEBUG_NETCHECK")
)
// The various default timeouts for things.
@@ -113,6 +113,10 @@ type Report struct {
GlobalV4 string // ip:port of global IPv4
GlobalV6 string // [ip]:port of global IPv6
// CaptivePortal is set when we think there's a captive portal that is
// intercepting HTTP traffic.
CaptivePortal opt.Bool
// TODO: update Clone when adding new fields
}
@@ -176,6 +180,10 @@ type Client struct {
// If nil, portmap discovery is not done.
PortMapper *portmapper.Client // lazily initialized on first use
// For tests
testEnoughRegions int
testCaptivePortalDelay time.Duration
mu sync.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map[time.Time]*Report // some previous reports
@@ -193,6 +201,9 @@ type STUNConn interface {
}
func (c *Client) enoughRegions() int {
if c.testEnoughRegions > 0 {
return c.testEnoughRegions
}
if c.Verbose {
// Abuse verbose a bit here so netcheck can show all region latencies
// in verbose mode.
@@ -201,6 +212,14 @@ func (c *Client) enoughRegions() int {
return 3
}
func (c *Client) captivePortalDelay() time.Duration {
if c.testCaptivePortalDelay > 0 {
return c.testCaptivePortalDelay
}
// Chosen semi-arbitrarily
return 200 * time.Millisecond
}
func (c *Client) logf(format string, a ...any) {
if c.Logf != nil {
c.Logf(format, a...)
@@ -210,7 +229,7 @@ func (c *Client) logf(format string, a ...any) {
}
func (c *Client) vlogf(format string, a ...any) {
if c.Verbose || debugNetcheck {
if c.Verbose || debugNetcheck() {
c.logf(format, a...)
}
}
@@ -784,13 +803,35 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
}
c.curState = rs
last := c.last
// Even if we're doing a non-incremental update, we may want to try our
// preferred DERP region for captive portal detection. Save that, if we
// have it.
var preferredDERP int
if last != nil {
preferredDERP = last.PreferredDERP
}
now := c.timeNow()
doFull := false
if c.nextFull || now.Sub(c.lastFull) > 5*time.Minute {
doFull = true
}
// If the last report had a captive portal and reported no UDP access,
// it's possible that we didn't get a useful netcheck due to the
// captive portal blocking us. If so, make this report a full
// (non-incremental) one.
if !doFull && last != nil {
doFull = !last.UDP && last.CaptivePortal.EqualBool(true)
}
if doFull {
last = nil // causes makeProbePlan below to do a full (initial) plan
c.nextFull = false
c.lastFull = now
metricNumGetReportFull.Add(1)
}
rs.incremental = last != nil
c.mu.Unlock()
@@ -875,6 +916,48 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
plan := makeProbePlan(dm, ifState, last)
// If we're doing a full probe, also check for a captive portal. We
// delay by a bit to wait for UDP STUN to finish, to avoid the probe if
// it's unnecessary.
captivePortalDone := syncs.ClosedChan()
captivePortalStop := func() {}
if !rs.incremental {
// NOTE(andrew): we can't simply add this goroutine to the
// `NewWaitGroupChan` below, since we don't wait for that
// waitgroup to finish when exiting this function and thus get
// a data race.
ch := make(chan struct{})
captivePortalDone = ch
tmr := time.AfterFunc(c.captivePortalDelay(), func() {
defer close(ch)
found, err := c.checkCaptivePortal(ctx, dm, preferredDERP)
if err != nil {
c.logf("[v1] checkCaptivePortal: %v", err)
return
}
rs.report.CaptivePortal.Set(found)
})
captivePortalStop = func() {
// Don't cancel our captive portal check if we're
// explicitly doing a verbose netcheck.
if c.Verbose {
return
}
if tmr.Stop() {
// Stopped successfully; need to close the
// signal channel ourselves.
close(ch)
return
}
// Did not stop; do nothing and it'll finish by itself
// and close the signal channel.
}
}
wg := syncs.NewWaitGroupChan()
wg.Add(len(plan))
for _, probeSet := range plan {
@@ -895,9 +978,17 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
case <-stunTimer.C:
case <-ctx.Done():
case <-wg.DoneChan():
// All of our probes finished, so if we have >0 responses, we
// stop our captive portal check.
if rs.anyUDP() {
captivePortalStop()
}
case <-rs.stopProbeCh:
// Saw enough regions.
c.vlogf("saw enough regions; not waiting for rest")
// We can stop the captive portal check since we know that we
// got a bunch of STUN responses.
captivePortalStop()
}
rs.waitHairCheck(ctx)
@@ -966,6 +1057,9 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
wg.Wait()
}
// Wait for captive portal check before finishing the report.
<-captivePortalDone
return c.finishAndStoreReport(rs, dm), nil
}
@@ -980,6 +1074,54 @@ func (c *Client) finishAndStoreReport(rs *reportState, dm *tailcfg.DERPMap) *Rep
return report
}
var noRedirectClient = &http.Client{
// No redirects allowed
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
// Remaining fields are the same as the default client.
Transport: http.DefaultClient.Transport,
Jar: http.DefaultClient.Jar,
Timeout: http.DefaultClient.Timeout,
}
// checkCaptivePortal reports whether or not we think the system is behind a
// captive portal, detected by making a request to a URL that we know should
// return a "204 No Content" response and checking if that's what we get.
//
// The boolean return is whether we think we have a captive portal.
func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, preferredDERP int) (bool, error) {
defer noRedirectClient.CloseIdleConnections()
// If we have a preferred DERP region with more than one node, try
// that; otherwise, pick a random one not marked as "Avoid".
if preferredDERP == 0 || dm.Regions[preferredDERP] == nil ||
(preferredDERP != 0 && len(dm.Regions[preferredDERP].Nodes) == 0) {
rids := make([]int, 0, len(dm.Regions))
for id, reg := range dm.Regions {
if reg == nil || reg.Avoid || len(reg.Nodes) == 0 {
continue
}
rids = append(rids, id)
}
preferredDERP = rids[rand.Intn(len(rids))]
}
node := dm.Regions[preferredDERP].Nodes[0]
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+node.HostName+"/generate_204", nil)
if err != nil {
return false, err
}
r, err := noRedirectClient.Do(req)
if err != nil {
return false, err
}
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
return r.StatusCode != 204, nil
}
// runHTTPOnlyChecks is the netcheck done by environments that can
// only do HTTP requests, such as ws/wasm.
func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *reportState, dm *tailcfg.DERPMap) error {
@@ -1096,7 +1238,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
}
_, err = io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8<<10))
_, err = io.Copy(io.Discard, io.LimitReader(resp.Body, 8<<10))
if err != nil {
return 0, ip, err
}
@@ -1201,6 +1343,9 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
if r.GlobalV6 != "" {
fmt.Fprintf(w, " v6a=%v", r.GlobalV6)
}
if r.CaptivePortal != "" {
fmt.Fprintf(w, " captiveportal=%v", r.CaptivePortal)
}
fmt.Fprintf(w, " derp=%v", r.PreferredDERP)
if r.PreferredDERP != 0 {
fmt.Fprintf(w, " derpdist=")

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