Compare commits

..

1 Commits

Author SHA1 Message Date
David Crawshaw
ad10cd71a6 tsweb: when unwrapping HTTPError, record the user-facing message also in the log
There's often some useful piece of information in there not already
repeated in the internal error.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-17 09:57:36 -04:00
707 changed files with 21183 additions and 96946 deletions

View File

@@ -1 +0,0 @@
suppress_failure_on_regression: true

1
.gitattributes vendored
View File

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

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: ''
assignees: ''
---
<!-- Please note, this template is for definite bugs, not requests for
support. If you need help with Tailscale, please email
support@tailscale.com. We don't provide support via Github issues. -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version information:**
- Device: [e.g. iPhone X, laptop]
- OS: [e.g. Windows, MacOS]
- OS version: [e.g. Windows 10, Ubuntu 18.04]
- Tailscale version: [e.g. 0.95-0]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,75 +0,0 @@
name: Bug report
description: File a bug report
labels: [needs-triage, bug]
body:
- type: markdown
attributes:
value: |
Please check if your bug is [already filed](https://github.com/tailscale/tailscale/issues).
Have an urgent issue? Let us know by emailing us at <support@tailscale.com>.
- type: textarea
id: what-happened
attributes:
label: What is the issue?
description: What happened? What did you expect to happen?
placeholder: oh no
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: What are the steps you took that hit this issue?
validations:
required: false
- type: textarea
id: changes
attributes:
label: Are there any recent changes that introduced the issue?
description: If so, what are those changes?
validations:
required: false
- type: dropdown
id: os
attributes:
label: OS
description: What OS are you using? You may select more than one.
multiple: true
options:
- Linux
- macOS
- Windows
- iOS
- Android
- Synology
- Other
validations:
required: false
- type: input
id: os-version
attributes:
label: OS version
description: What OS version are you using?
placeholder: e.g., Debian 11.0, macOS Big Sur 11.6, Synology DSM 7
validations:
required: false
- type: input
id: ts-version
attributes:
label: Tailscale version
description: What Tailscale version are you using?
placeholder: e.g., 1.14.4
validations:
required: false
- type: input
id: bug-report
attributes:
label: Bug report
description: Please run [`tailscale bugreport`](https://tailscale.com/kb/1080/cli/?q=Cli#bugreport) and share the bug identifier. The identifier is a random string which allows Tailscale support to locate your account and gives a point to focus on when looking for errors.
placeholder: e.g., BUG-1b7641a16971a9cd75822c0ed8043fee70ae88cf05c52981dc220eb96a5c49a8-20210427151443Z-fbcd4fd3a4b7ad94
validations:
required: false
- type: markdown
attributes:
value: |
Thanks for filing a bug report!

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Support
url: https://tailscale.com/contact/support/
about: Contact us for support
- name: Troubleshooting
- name: Support and Product Questions
url: https://tailscale.com/kb/1023/troubleshooting
about: Troubleshoot common issues
about: Please send support questions and questions about the Tailscale product to support@tailscale.com

View File

@@ -0,0 +1,26 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or
features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,42 +0,0 @@
name: Feature request
description: Propose a new feature
title: "FR: "
labels: [needs-triage, fr]
body:
- type: markdown
attributes:
value: |
Please check if your feature request is [already filed](https://github.com/tailscale/tailscale/issues).
Tell us about your idea!
- type: textarea
id: problem
attributes:
label: What are you trying to do?
description: Tell us about the problem you're trying to solve.
validations:
required: false
- type: textarea
id: solution
attributes:
label: How should we solve this?
description: If you have an idea of how you'd like to see this feature work, let us know.
validations:
required: false
- type: textarea
id: alternative
attributes:
label: What is the impact of not solving this?
description: (How) Are you currently working around the issue?
validations:
required: false
- type: textarea
id: context
attributes:
label: Anything else?
description: Any additional context to share, e.g., links
validations:
required: false
- type: markdown
attributes:
value: |
Thanks for filing a feature request!

View File

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

View File

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

View File

@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main, release-branch/* ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '31 14 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
id: go
- name: Check out code into the Go module directory
@@ -37,12 +37,6 @@ jobs:
GOARCH: amd64
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
- name: iOS build most
env:
GOOS: ios
GOARCH: arm64
run: go install ./ipn/... ./wgengine/ ./types/... ./control/controlclient
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -14,9 +14,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env sh
#
# This is a temporary hack to work around
# https://github.com/golang/go/issues/51629 , wherein the stringer
# generator doesn't work with generics.
#
# This script is the equivalent of `go generate ./...`, except that it
# only runs generate on packages that don't try to use stringer.
set -e
find . -name '*.go' | xargs grep -l go:generate | xargs -n1 dirname | sort -u | while read dir; do
if ! egrep "cmd/(stringer|cloner)" $dir/*.go; then
set -x
go generate -tags=hermetic $dir
set +x
fi
done

View File

@@ -1,37 +0,0 @@
name: go generate
on:
push:
branches:
- main
- "release-branch/*"
pull_request:
branches:
- "*"
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: check 'go generate' is clean
# The shell script invocation below is a temporary hack for
# https://github.com/tailscale/tailscale/issues/4194. When
# that issue is fixed, replace its invocation with:
# go generate --tags=hermetic ./...
run: |
set -e
./.github/workflows/go-generate-without-stringer.sh
echo
echo
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)

View File

@@ -14,9 +14,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1

View File

@@ -1,63 +0,0 @@
name: Linux race
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Basic build
run: go build ./cmd/...
- name: Run tests and benchmarks with -race flag on linux
run: go test -race -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"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

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

View File

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

View File

@@ -14,9 +14,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v1
with:
go-version: 1.18
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1
@@ -24,35 +24,11 @@ jobs:
- name: Run go vet
run: go vet ./...
- name: Install staticcheck
run: "GOBIN=~/.local/bin go install honnef.co/go/tools/cmd/staticcheck"
- name: Print staticcheck version
run: "staticcheck -version"
run: go run honnef.co/go/tools/cmd/staticcheck -version
- name: Run staticcheck (linux/amd64)
env:
GOOS: linux
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (darwin/amd64)
env:
GOOS: darwin
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/amd64)
env:
GOOS: windows
GOARCH: amd64
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/386)
env:
GOOS: windows
GOARCH: "386"
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck
run: "go run honnef.co/go/tools/cmd/staticcheck -- $(go list ./... | grep -v tempfork)"
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -1,46 +0,0 @@
name: VM
on:
pull_request:
branches:
- '*'
jobs:
ubuntu2004-LTS-cloud-base:
runs-on: [ self-hosted, linux, vm ]
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set GOPATH
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v2.1.5
with:
go-version: 1.18
- name: Checkout Code
uses: actions/checkout@v1
- 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"
- 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

@@ -1,66 +0,0 @@
name: Windows race
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
test:
runs-on: windows-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Install Go
uses: actions/setup-go@v2.1.5
with:
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v2
with:
# Note: unlike some other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
# The -race- here ensures that non-race builds and race builds do not
# overwrite each others cache, as while they share some files, they
# differ in most by volume (build cache).
# TODO(raggi): add a go version here.
key: ${{ runner.os }}-go-2-race-${{ hashFiles('**/go.sum') }}
- name: Test with -race flag
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: go test -race -bench . -benchtime 1x ./...
- 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

@@ -1,63 +0,0 @@
name: Windows
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
test:
runs-on: windows-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Install Go
uses: actions/setup-go@v2.1.5
with:
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v2
with:
# Note: unlike some other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
# TODO(raggi): add a go version here.
key: ${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
- name: Test
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: go test -bench . -benchtime 1x ./...
- 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'

8
.gitignore vendored
View File

@@ -1,12 +1,12 @@
# Binaries for programs and plugins
*~
*.tmp
*.exe
*.dll
*.so
*.dylib
*.spk
cmd/relaynode/relaynode
cmd/taillogin/taillogin
cmd/tailscale/tailscale
cmd/tailscaled/tailscaled
@@ -18,7 +18,3 @@ cmd/tailscaled/tailscaled
# Dependency directories (remove the comment below to include it)
# vendor/
# direnv config, this may be different for other people so it's probably safer
# to make this nonspecific.
.envrc

View File

@@ -2,26 +2,15 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
############################################################################
#
# WARNING: Tailscale is not yet officially supported in container
# environments, such as Docker and Kubernetes. Though it should work, we
# don't regularly test it, and we know there are some feature limitations.
#
# See current bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
# This Dockerfile includes all the tailscale binaries.
#
# To build the Dockerfile:
#
# $ docker build -t tailscale/tailscale .
# $ docker build -t tailscale:tailscale .
#
# To run the tailscaled agent:
#
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale/tailscale tailscaled
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale:tailscale tailscaled
#
# To then log in:
#
@@ -32,29 +21,18 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.18-alpine AS build-env
FROM golang:1.14-alpine AS build-env
WORKDIR /go/src/tailscale
COPY go.mod go.sum ./
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
# see build_docker.sh
ARG VERSION_LONG=""
ENV VERSION_LONG=$VERSION_LONG
ARG VERSION_SHORT=""
ENV VERSION_SHORT=$VERSION_SHORT
ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN go install -v ./cmd/...
RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled
FROM ghcr.io/tailscale/alpine-base:3.14
FROM alpine:3.11
RUN apk add --no-cache ca-certificates iptables iproute2
COPY --from=build-env /go/bin/* /usr/local/bin/

View File

@@ -1,6 +0,0 @@
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
FROM alpine:3.15
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

46
LICENSE
View File

@@ -1,29 +1,27 @@
BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS.
All rights reserved.
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Tailscale Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,46 +1,18 @@
IMAGE_REPO ?= tailscale/tailscale
SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
usage:
echo "See Makefile"
vet:
./tool/go vet ./...
go vet ./...
updatedeps:
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
depaware:
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
buildwindows:
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
build386:
GOOS=linux GOARCH=386 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildlinuxarm:
GOOS=linux GOARCH=arm ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
./build_docker.sh
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
check: staticcheck vet depaware
staticcheck:
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
spk:
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
spkall:
mkdir -p spks
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
pushspk: spk
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
scp tailscale.spk root@${SYNO_HOST}:
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)

View File

@@ -8,12 +8,11 @@ Private WireGuard® networks made easy
This repository contains all the open source Tailscale client code and
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
daemon runs primarily on Linux; it also works to varying degrees on
FreeBSD, OpenBSD, Darwin, and Windows.
The Android app is at https://github.com/tailscale/tailscale-android
The Synology package is at https://github.com/tailscale/tailscale-synology
## Using
We serve packages for a variety of distros at
@@ -31,20 +30,8 @@ wrappers that are not open source.
go install tailscale.com/cmd/tailscale{,d}
```
If you're packaging Tailscale for distribution, use `build_dist.sh`
instead, to burn commit IDs and version info into the binaries:
```
./build_dist.sh tailscale.com/cmd/tailscale
./build_dist.sh tailscale.com/cmd/tailscaled
```
If your distro has conventions that preclude the use of
`build_dist.sh`, please do the equivalent of what it does in your
distro's way, so that bug reports contain useful version information.
We only guarantee to support the latest Go release and any Go beta or
release candidate builds (currently Go 1.18) in module mode. It might
release candidate builds (currently Go 1.15) in module mode. It might
work in earlier Go versions or in GOPATH mode, but we're making no
effort to keep those working.
@@ -64,13 +51,8 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
## About Us
[Tailscale](https://tailscale.com/) is primarily developed by the
people at https://github.com/orgs/tailscale/people. For other contributors,
see:
* https://github.com/tailscale/tailscale/graphs/contributors
* https://github.com/tailscale/tailscale-android/graphs/contributors
## Legal
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
from Tailscale Inc.
You can learn more about us from [our website](https://tailscale.com).
WireGuard is a registered trademark of Jason A. Donenfeld.

View File

@@ -1 +0,0 @@
1.23.0

1105
api.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for binary distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries, so that we can track down user
# issues.
#
# If you're packaging Tailscale for a distro, please consider using
# this script, or executing equivalent commands in your
# distro-specific build system.
set -eu
IFS=".$IFS" read -r major minor patch <VERSION.txt
git_hash=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
git_hash="${git_hash}-dirty"
fi
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
change_count=$(git rev-list --count HEAD "^$base_hash")
short_hash=$(echo "$git_hash" | cut -c1-9)
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
change_suffix="-$change_count"
else
change_suffix=""
fi
long_suffix="$change_suffix-t$short_hash"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"
EOF
exit 0
fi
exec ./tool/go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for docker distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries inside docker, so that we can track down user
# issues.
#
############################################################################
#
# WARNING: Tailscale is not yet officially supported in container
# environments, such as Docker and Kubernetes. Though it should work, we
# don't regularly test it, and we know there are some feature limitations.
#
# See current bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
set -eu
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
export PATH=$PWD/tool:$PATH
eval $(./build_dist.sh shellvars)
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
PUSH="${PUSH:-false}"
REPOS="${REPOS:-${DEFAULT_REPOS}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
go run github.com/tailscale/mkctr@latest \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}"

View File

@@ -1,129 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package chirp implements a client to communicate with the BIRD Internet
// Routing Daemon.
package chirp
import (
"bufio"
"fmt"
"net"
"strings"
)
// New creates a BIRDClient.
func New(socket string) (*BIRDClient, error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
}
b := &BIRDClient{socket: socket, conn: conn, scanner: bufio.NewScanner(conn)}
// Read and discard the first line as that is the welcome message.
if _, err := b.readResponse(); err != nil {
return nil, err
}
return b, nil
}
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
type BIRDClient struct {
socket string
conn net.Conn
scanner *bufio.Scanner
}
// Close closes the underlying connection to BIRD.
func (b *BIRDClient) Close() error { return b.conn.Close() }
// DisableProtocol disables the provided protocol.
func (b *BIRDClient) DisableProtocol(protocol string) error {
out, err := b.exec("disable %s", protocol)
if err != nil {
return err
}
if strings.Contains(out, fmt.Sprintf("%s: already disabled", protocol)) {
return nil
} else if strings.Contains(out, fmt.Sprintf("%s: disabled", protocol)) {
return nil
}
return fmt.Errorf("failed to disable %s: %v", protocol, out)
}
// EnableProtocol enables the provided protocol.
func (b *BIRDClient) EnableProtocol(protocol string) error {
out, err := b.exec("enable %s", protocol)
if err != nil {
return err
}
if strings.Contains(out, fmt.Sprintf("%s: already enabled", protocol)) {
return nil
} else if strings.Contains(out, fmt.Sprintf("%s: enabled", protocol)) {
return nil
}
return fmt.Errorf("failed to enable %s: %v", protocol, out)
}
// BIRD CLI docs from https://bird.network.cz/?get_doc&v=20&f=prog-2.html#ss2.9
// Each session of the CLI consists of a sequence of request and replies,
// slightly resembling the FTP and SMTP protocols.
// Requests are commands encoded as a single line of text,
// replies are sequences of lines starting with a four-digit code
// followed by either a space (if it's the last line of the reply) or
// a minus sign (when the reply is going to continue with the next line),
// the rest of the line contains a textual message semantics of which depends on the numeric code.
// If a reply line has the same code as the previous one and it's a continuation line,
// the whole prefix can be replaced by a single white space character.
//
// Reply codes starting with 0 stand for action successfully completed messages,
// 1 means table entry, 8 runtime error and 9 syntax error.
func (b *BIRDClient) exec(cmd string, args ...any) (string, error) {
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
return "", err
}
fmt.Fprintln(b.conn)
return b.readResponse()
}
// hasResponseCode reports whether the provided byte slice is
// prefixed with a BIRD response code.
// Equivalent regex: `^\d{4}[ -]`.
func hasResponseCode(s []byte) bool {
if len(s) < 5 {
return false
}
for _, b := range s[:4] {
if '0' <= b && b <= '9' {
continue
}
return false
}
return s[4] == ' ' || s[4] == '-'
}
func (b *BIRDClient) readResponse() (string, error) {
var resp strings.Builder
var done bool
for !done {
if !b.scanner.Scan() {
return "", fmt.Errorf("reading response from bird failed: %q", resp.String())
}
if err := b.scanner.Err(); err != nil {
return "", err
}
out := b.scanner.Bytes()
if _, err := resp.Write(out); err != nil {
return "", err
}
if hasResponseCode(out) {
done = out[4] == ' '
}
if !done {
resp.WriteRune('\n')
}
}
return resp.String(), nil
}

View File

@@ -1,111 +0,0 @@
// 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 chirp
import (
"bufio"
"errors"
"fmt"
"net"
"path/filepath"
"strings"
"testing"
)
type fakeBIRD struct {
net.Listener
protocolsEnabled map[string]bool
sock string
}
func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD {
sock := filepath.Join(t.TempDir(), "sock")
l, err := net.Listen("unix", sock)
if err != nil {
t.Fatal(err)
}
pe := make(map[string]bool)
for _, p := range protocols {
pe[p] = false
}
return &fakeBIRD{
Listener: l,
protocolsEnabled: pe,
sock: sock,
}
}
func (fb *fakeBIRD) listen() error {
for {
c, err := fb.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
go fb.handle(c)
}
}
func (fb *fakeBIRD) handle(c net.Conn) {
fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.")
sc := bufio.NewScanner(c)
for sc.Scan() {
cmd := sc.Text()
args := strings.Split(cmd, " ")
switch args[0] {
case "enable":
en, ok := fb.protocolsEnabled[args[1]]
if !ok {
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
} else if en {
fmt.Fprintf(c, "0010-%s: already enabled\n", args[1])
} else {
fmt.Fprintf(c, "0011-%s: enabled\n", args[1])
}
fmt.Fprintln(c, "0000 ")
fb.protocolsEnabled[args[1]] = true
case "disable":
en, ok := fb.protocolsEnabled[args[1]]
if !ok {
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
} else if !en {
fmt.Fprintf(c, "0008-%s: already disabled\n", args[1])
} else {
fmt.Fprintf(c, "0009-%s: disabled\n", args[1])
}
fmt.Fprintln(c, "0000 ")
fb.protocolsEnabled[args[1]] = false
}
}
}
func TestChirp(t *testing.T) {
fb := newFakeBIRD(t, "tailscale")
defer fb.Close()
go fb.listen()
c, err := New(fb.sock)
if err != nil {
t.Fatal(err)
}
if err := c.EnableProtocol("tailscale"); err != nil {
t.Fatal(err)
}
if err := c.EnableProtocol("tailscale"); err != nil {
t.Fatal(err)
}
if err := c.DisableProtocol("tailscale"); err != nil {
t.Fatal(err)
}
if err := c.DisableProtocol("tailscale"); err != nil {
t.Fatal(err)
}
if err := c.EnableProtocol("rando"); err == nil {
t.Fatalf("enabling %q succeded", "rando")
}
if err := c.DisableProtocol("rando"); err == nil {
t.Fatalf("disabling %q succeded", "rando")
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package apitype contains types for the Tailscale local API.
package apitype
import "tailscale.com/tailcfg"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
}
// FileTarget is a node to which files can be sent, and the PeerAPI
// URL base to do so via.
type FileTarget struct {
Node *tailcfg.Node
// PeerAPI is the http://ip:port URL base of the node's peer API,
// without any path (not even a single slash).
PeerAPIURL string
}
type WaitingFile struct {
Name string
Size int64
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The servetls program shows how to run an HTTPS server
// using a Tailscale cert via LetsEncrypt.
package main
import (
"crypto/tls"
"io"
"log"
"net/http"
"tailscale.com/client/tailscale"
)
func main() {
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: tailscale.GetCertificate,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<h1>Hello from Tailscale!</h1> It works.")
}),
}
log.Printf("Running TLS server on :443 ...")
log.Fatal(s.ListenAndServeTLS("", ""))
}

View File

@@ -1,12 +0,0 @@
// 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.
//go:build !go1.18
// +build !go1.18
package tailscale
func init() {
you_need_Go_1_18_to_compile_Tailscale()
}

View File

@@ -1,529 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
// Package tailscale contains Tailscale client code.
package tailscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"time"
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
TailscaledSocketSetExplicitly bool
// TailscaledDialer is the DialContext func that connects to the local machine's
// tailscaled or equivalent.
TailscaledDialer = defaultDialer
)
func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
// TODO: make this part of a safesocket.ConnectionStrategy
if !TailscaledSocketSetExplicitly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
}
var (
// tsClient does HTTP requests to the local Tailscale daemon.
// We lazily initialize the client in case the caller wants to
// override TailscaledDialer.
tsClient *http.Client
tsClientOnce sync.Once
)
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
//
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
//
// The hostname must be "local-tailscaled.sock", even though it
// doesn't actually do any DNS lookup. The actual means of connecting to and
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func DoLocalRequest(req *http.Request) (*http.Response, error) {
tsClientOnce.Do(func() {
tsClient = &http.Client{
Transport: &http.Transport{
DialContext: TailscaledDialer,
},
}
})
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
return tsClient.Do(req)
}
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
if res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
path := req.URL.Path
pathPrefix, _, _ := strings.Cut(path, "?")
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
type errorJSON struct {
Error string
}
// AccessDeniedError is an error due to permissions.
type AccessDeniedError struct {
err error
}
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
func (e *AccessDeniedError) Unwrap() error { return e.err }
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
func IsAccessDeniedError(err error) bool {
var ae *AccessDeniedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return errors.New(j.Error)
}
return err
}
func errorMessageFromBody(body []byte) string {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return j.Error
}
return strings.TrimSpace(string(body))
}
var onVersionMismatch func(clientVer, serverVer string)
// SetVersionMismatchHandler sets f as the version mismatch handler
// to be called when the client (the current process) has a version
// number that doesn't match the server's declared version.
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return nil, err
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != wantStatus {
return nil, bestError(err, slurp)
}
return slurp, nil
}
func get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
return nil, err
}
r := new(apitype.WhoIsResponse)
if err := json.Unmarshal(body, r); err != nil {
if max := 200; len(body) > max {
body = append(body[:max], "..."...)
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
}
return r, nil
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func DaemonMetrics(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/metrics")
}
// Profile returns a pprof profile of the Tailscale daemon.
func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
}
if sec != 0 || pprofType == "profile" {
secArg = fmt.Sprint(sec)
}
return get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func BugReport(ctx context.Context, note string) (string, error) {
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func DebugAction(ctx context.Context, action string) error {
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "?peers=false")
}
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
st := new(ipnstate.Status)
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
}
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
}
func DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return nil, 0, err
}
if res.ContentLength == -1 {
res.Body.Close()
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, res.ContentLength, nil
}
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
}
var fts []apitype.FileTarget
if err := json.Unmarshal(body, &fts); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
return fts, nil
}
// PushFile sends Taildrop file r to target.
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
}
func CheckIPForwarding(ctx context.Context) error {
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
if err != nil {
return nil, err
}
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//
// It ends in a punctuation. See caller.
func tailscaledConnectHint() string {
if runtime.GOOS != "linux" {
// TODO(bradfitz): flesh this out
return "not running?"
}
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
if err != nil {
return "not running?"
}
// Parse:
// LoadState=loaded
// ActiveState=inactive
// SubState=dead
st := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
st[k] = strings.TrimSpace(v)
}
}
if st["LoadState"] == "loaded" &&
(st["SubState"] != "running" || st["ActiveState"] != "active") {
return "systemd tailscaled.service not running."
}
return "not running?"
}

View File

@@ -1,77 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program addlicense adds a license header to a file.
// It is intended for use with 'go generate',
// so it has a slightly weird usage.
package main
import (
"flag"
"fmt"
"os"
"os/exec"
)
var (
year = flag.Int("year", 0, "copyright year")
file = flag.String("file", "", "file to modify")
)
func usage() {
fmt.Fprintf(os.Stderr, `
usage: addlicense -year YEAR -file FILE <subcommand args...>
`[1:])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, `
addlicense adds a Tailscale license to the beginning of file,
using year as the copyright year.
It is intended for use with 'go generate', so it also runs a subcommand,
which presumably creates the file.
Sample usage:
addlicense -year 2021 -file pull_strings.go stringer -type=pull
`[1:])
os.Exit(2)
}
func main() {
flag.Usage = usage
flag.Parse()
if len(flag.Args()) == 0 {
flag.Usage()
}
cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
check(err)
b, err := os.ReadFile(*file)
check(err)
f, err := os.OpenFile(*file, os.O_TRUNC|os.O_WRONLY, 0644)
check(err)
_, err = fmt.Fprintf(f, license, *year)
check(err)
_, err = f.Write(b)
check(err)
err = f.Close()
check(err)
}
func check(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
var license = `
// Copyright (c) %d 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.
`[1:]

View File

@@ -17,20 +17,22 @@ import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/format"
"go/token"
"go/types"
"io/ioutil"
"log"
"os"
"strings"
"golang.org/x/tools/go/packages"
"tailscale.com/util/codegen"
)
var (
flagTypes = flag.String("type", "", "comma-separated list of types; required")
flagOutput = flag.String("output", "", "output file; required")
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
)
func main() {
@@ -60,55 +62,64 @@ func main() {
pkg := pkgs[0]
buf := new(bytes.Buffer)
imports := make(map[string]struct{})
namedTypes := codegen.NamedTypes(pkg)
for _, typeName := range typeNames {
typ, ok := namedTypes[typeName]
if !ok {
found := false
for _, file := range pkg.Syntax {
//var fbuf bytes.Buffer
//ast.Fprint(&fbuf, pkg.Fset, file, nil)
//fmt.Println(fbuf.String())
for _, d := range file.Decls {
decl, ok := d.(*ast.GenDecl)
if !ok || decl.Tok != token.TYPE {
continue
}
for _, s := range decl.Specs {
spec, ok := s.(*ast.TypeSpec)
if !ok || spec.Name.Name != typeName {
continue
}
typeNameObj := pkg.TypesInfo.Defs[spec.Name]
typ, ok := typeNameObj.Type().(*types.Named)
if !ok {
continue
}
pkg := typeNameObj.Pkg()
gen(buf, imports, typeName, typ, pkg)
found = true
}
}
}
if !found {
log.Fatalf("could not find type %s", typeName)
}
gen(buf, imports, typ, pkg.Types)
}
w := func(format string, args ...any) {
w := func(format string, args ...interface{}) {
fmt.Fprintf(buf, format+"\n", args...)
}
if *flagCloneFunc {
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src any) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
}
w(" }")
w(" return false")
w("}")
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src interface{}) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
}
w(" }")
w(" return false")
w("}")
contents := new(bytes.Buffer)
var flagArgs []string
if *flagTypes != "" {
flagArgs = append(flagArgs, "-type="+*flagTypes)
}
if *flagOutput != "" {
flagArgs = append(flagArgs, "-output="+*flagOutput)
}
if *flagBuildTags != "" {
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
}
if *flagCloneFunc {
flagArgs = append(flagArgs, "-clonefunc")
}
fmt.Fprintf(contents, header, strings.Join(flagArgs, " "), pkg.Name)
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
fmt.Fprintf(contents, "import (\n")
for s := range imports {
fmt.Fprintf(contents, "\t%q\n", s)
@@ -116,12 +127,17 @@ func main() {
fmt.Fprintf(contents, ")\n\n")
contents.Write(buf.Bytes())
out, err := format.Source(contents.Bytes())
if err != nil {
log.Fatalf("%s, in source:\n%s", err, contents.Bytes())
}
output := *flagOutput
if output == "" {
flag.Usage()
os.Exit(2)
}
if err := codegen.WriteFormatted(contents.Bytes(), output); err != nil {
if err := ioutil.WriteFile(output, out, 0666); err != nil {
log.Fatal(err)
}
}
@@ -130,14 +146,13 @@ const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserve
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
//` + `go:generate` + ` go run tailscale.com/cmd/cloner %s
// Code generated by tailscale.com/cmd/cloner -type %s; DO NOT EDIT.
package %s
`
func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisPkg *types.Package) {
func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types.Named, thisPkg *types.Package) {
pkgQual := func(pkg *types.Package) string {
if thisPkg == pkg {
return ""
@@ -149,106 +164,105 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
return types.TypeString(t, pkgQual)
}
t, ok := typ.Underlying().(*types.Struct)
if !ok {
return
}
name := typ.Obj().Name()
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
writef := func(format string, args ...any) {
fmt.Fprintf(buf, "\t"+format+"\n", args...)
}
writef("if src == nil {")
writef("\treturn nil")
writef("}")
writef("dst := new(%s)", name)
writef("*dst = *src")
for i := 0; i < t.NumFields(); i++ {
fname := t.Field(i).Name()
ft := t.Field(i).Type()
if !codegen.ContainsPointers(ft) {
continue
switch t := typ.Underlying().(type) {
case *types.Struct:
// We generate two bits of code simultaneously while we walk the struct.
// One is the Clone method itself, which we write directly to buf.
// The other is a variable assignment that will fail if the struct
// changes without the Clone method getting regenerated.
// We write that to regenBuf, and then append it to buf at the end.
regenBuf := new(bytes.Buffer)
writeRegen := func(format string, args ...interface{}) {
fmt.Fprintf(regenBuf, format+"\n", args...)
}
if named, _ := ft.(*types.Named); named != nil {
if isViewType(ft) {
writef("dst.%s = src.%s", fname, fname)
writeRegen("// A compilation failure here means this code must be regenerated, with command:")
writeRegen("// tailscale.com/cmd/cloner -type %s", *flagTypes)
writeRegen("var _%sNeedsRegeneration = %s(struct {", name, name)
name := typ.Obj().Name()
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
writef := func(format string, args ...interface{}) {
fmt.Fprintf(buf, "\t"+format+"\n", args...)
}
writef("if src == nil {")
writef("\treturn nil")
writef("}")
writef("dst := new(%s)", name)
writef("*dst = *src")
for i := 0; i < t.NumFields(); i++ {
fname := t.Field(i).Name()
ft := t.Field(i).Type()
writeRegen("\t%s %s", fname, importedName(ft))
if !containsPointers(ft) {
continue
}
if !hasBasicUnderlying(ft) {
if named, _ := ft.(*types.Named); named != nil && !hasBasicUnderlying(ft) {
writef("dst.%s = *src.%s.Clone()", fname, fname)
continue
}
}
switch ft := ft.Underlying().(type) {
case *types.Slice:
if codegen.ContainsPointers(ft.Elem()) {
n := importedName(ft.Elem())
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
switch ft := ft.Underlying().(type) {
case *types.Slice:
if containsPointers(ft.Elem()) {
n := importedName(ft.Elem())
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
writef("}")
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
}
case *types.Pointer:
if named, _ := ft.Elem().(*types.Named); named != nil && containsPointers(ft.Elem()) {
writef("dst.%s = src.%s.Clone()", fname, fname)
continue
}
n := importedName(ft.Elem())
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = new(%s)", fname, n)
writef("\t*dst.%s = *src.%s", fname, fname)
if containsPointers(ft.Elem()) {
writef("\t" + `panic("TODO pointers in pointers")`)
}
writef("}")
} else {
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
case *types.Map:
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
n := importedName(sliceType.Elem())
writef("\tfor k := range src.%s {", fname)
// use zero-length slice instead of nil to ensure
// the key is always copied.
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
writef("\t}")
} else if containsPointers(ft.Elem()) {
writef("\t\t" + `panic("TODO map value pointers")`)
} else {
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v", fname)
writef("\t}")
}
writef("}")
case *types.Struct:
writef(`panic("TODO struct %s")`, fname)
default:
writef(`panic(fmt.Sprintf("TODO: %T", ft))`)
}
case *types.Pointer:
if named, _ := ft.Elem().(*types.Named); named != nil && codegen.ContainsPointers(ft.Elem()) {
writef("dst.%s = src.%s.Clone()", fname, fname)
continue
}
n := importedName(ft.Elem())
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = new(%s)", fname, n)
writef("\t*dst.%s = *src.%s", fname, fname)
if codegen.ContainsPointers(ft.Elem()) {
writef("\t" + `panic("TODO pointers in pointers")`)
}
writef("}")
case *types.Map:
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
n := importedName(sliceType.Elem())
writef("\tfor k := range src.%s {", fname)
// use zero-length slice instead of nil to ensure
// the key is always copied.
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
writef("\t}")
} else if codegen.ContainsPointers(ft.Elem()) {
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v.Clone()", fname)
writef("\t}")
} else {
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v", fname)
writef("\t}")
}
writef("}")
default:
writef(`panic("TODO: %s (%T)")`, fname, ft)
}
}
writef("return dst")
fmt.Fprintf(buf, "}\n\n")
writef("return dst")
fmt.Fprintf(buf, "}\n\n")
buf.Write(codegen.AssertStructUnchanged(t, thisPkg, name, "Clone", imports))
}
writeRegen("}{})\n")
func isViewType(typ types.Type) bool {
t, ok := typ.Underlying().(*types.Struct)
if !ok {
return false
buf.Write(regenBuf.Bytes())
}
if t.NumFields() != 1 {
return false
}
return t.Field(0).Name() == "ж"
}
func hasBasicUnderlying(typ types.Type) bool {
@@ -259,3 +273,34 @@ func hasBasicUnderlying(typ types.Type) bool {
return false
}
}
func containsPointers(typ types.Type) bool {
switch typ.String() {
case "time.Time":
// time.Time contains a pointer that does not need copying
return false
case "inet.af/netaddr.IP":
return false
}
switch ft := typ.Underlying().(type) {
case *types.Array:
return containsPointers(ft.Elem())
case *types.Chan:
return true
case *types.Interface:
return true // a little too broad
case *types.Map:
return true
case *types.Pointer:
return true
case *types.Slice:
return true
case *types.Struct:
for i := 0; i < ft.NumFields(); i++ {
if containsPointers(ft.Field(i).Type()) {
return true
}
}
}
return false
}

View File

@@ -1,67 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"expvar"
"log"
"net"
"net/http"
"strings"
"sync/atomic"
"time"
)
var dnsCache atomic.Value // of []byte
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
func refreshBootstrapDNSLoop() {
if *bootstrapDNS == "" {
return
}
for {
refreshBootstrapDNS()
time.Sleep(10 * time.Minute)
}
}
func refreshBootstrapDNS() {
if *bootstrapDNS == "" {
return
}
dnsEntries := make(map[string][]net.IP)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
names := strings.Split(*bootstrapDNS, ",")
var r net.Resolver
for _, name := range names {
addrs, err := r.LookupIP(ctx, "ip", name)
if err != nil {
log.Printf("bootstrap DNS lookup %q: %v", name, err)
continue
}
dnsEntries[name] = addrs
}
j, err := json.MarshalIndent(dnsEntries, "", "\t")
if err != nil {
// leave the old values in place
return
}
dnsCache.Store(j)
}
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
bootstrapDNSRequests.Add(1)
w.Header().Set("Content-Type", "application/json")
j, _ := dnsCache.Load().([]byte)
// Bootstrap DNS requests occur cross-regions,
// and are randomized per request,
// so keeping a connection open is pointlessly expensive.
w.Header().Set("Connection", "close")
w.Write(j)
}

View File

@@ -1,35 +0,0 @@
// 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 main
import (
"net/http"
"testing"
)
func BenchmarkHandleBootstrapDNS(b *testing.B) {
prev := *bootstrapDNS
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
defer func() {
*bootstrapDNS = prev
}()
refreshBootstrapDNS()
w := new(bitbucketResponseWriter)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(b *testing.PB) {
for b.Next() {
handleBootstrapDNS(w, nil)
}
})
}
type bitbucketResponseWriter struct{}
func (b *bitbucketResponseWriter) Header() http.Header { return make(http.Header) }
func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p), nil }
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}

View File

@@ -1,95 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"path/filepath"
"regexp"
"golang.org/x/crypto/acme/autocert"
)
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
type certProvider interface {
// TLSConfig creates a new TLS config suitable for net/http.Server servers.
TLSConfig() *tls.Config
// HTTPHandler handle ACME related request, if any.
HTTPHandler(fallback http.Handler) http.Handler
}
func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
if dir == "" {
return nil, errors.New("missing required --certdir flag")
}
switch mode {
case "letsencrypt":
certManager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(hostname),
Cache: autocert.DirCache(dir),
}
if hostname == "derp.tailscale.com" {
certManager.HostPolicy = prodAutocertHostPolicy
certManager.Email = "security@tailscale.com"
}
return certManager, nil
case "manual":
return NewManualCertManager(dir, hostname)
default:
return nil, fmt.Errorf("unsupport cert mode: %q", mode)
}
}
type manualCertManager struct {
cert *tls.Certificate
hostname string
}
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
func NewManualCertManager(certdir, hostname string) (certProvider, error) {
keyname := unsafeHostnameCharacters.ReplaceAllString(hostname, "")
crtPath := filepath.Join(certdir, keyname+".crt")
keyPath := filepath.Join(certdir, keyname+".key")
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
if err != nil {
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
}
// ensure hostname matches with the certificate
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, fmt.Errorf("can not load cert: %w", err)
}
if err := x509Cert.VerifyHostname(hostname); err != nil {
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
}
return &manualCertManager{cert: &cert, hostname: hostname}, nil
}
func (m *manualCertManager) TLSConfig() *tls.Config {
return &tls.Config{
Certificates: nil,
NextProtos: []string{
"h2", "http/1.1", // enable HTTP/2
},
GetCertificate: m.getCertificate,
}
}
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi.ServerName != m.hostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}
return m.cert, nil
}
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
return fallback
}

View File

@@ -13,10 +13,10 @@ import (
"expvar"
"flag"
"fmt"
"html"
"io"
"io/ioutil"
"log"
"math"
"net"
"net/http"
"os"
@@ -25,7 +25,8 @@ import (
"strings"
"time"
"golang.org/x/time/rate"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/acme/autocert"
"tailscale.com/atomicfile"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -34,72 +35,35 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/version"
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
addr = flag.String("a", ":443", "server address")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runSTUN = flag.Bool("stun", false, "also run a STUN server")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
)
var (
stats = new(metrics.Set)
stunDisposition = &metrics.LabelMap{Label: "disposition"}
stunAddrFamily = &metrics.LabelMap{Label: "family"}
tlsRequestVersion = &metrics.LabelMap{Label: "version"}
tlsActiveVersion = &metrics.LabelMap{Label: "version"}
stunReadError = stunDisposition.Get("read_error")
stunNotSTUN = stunDisposition.Get("not_stun")
stunWriteError = stunDisposition.Get("write_error")
stunSuccess = stunDisposition.Get("success")
stunIPv4 = stunAddrFamily.Get("ipv4")
stunIPv6 = stunAddrFamily.Get("ipv6")
)
func init() {
stats.Set("counter_requests", stunDisposition)
stats.Set("counter_addrfamily", stunAddrFamily)
expvar.Publish("stun", stats)
expvar.Publish("derper_tls_request_version", tlsRequestVersion)
expvar.Publish("gauge_derper_tls_active_version", tlsActiveVersion)
}
type config struct {
PrivateKey key.NodePrivate
PrivateKey wgcfg.PrivateKey
}
func loadConfig() config {
if *dev {
return config{PrivateKey: key.NewNode()}
return config{PrivateKey: mustNewKey()}
}
if *configPath == "" {
if os.Getuid() == 0 {
*configPath = "/var/lib/derper/derper.key"
} else {
log.Fatalf("derper: -c <config path> not specified")
}
log.Printf("no config path specified; using %s", *configPath)
log.Fatalf("derper: -c <config path> not specified")
}
b, err := ioutil.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
case os.IsNotExist(err):
return writeNewConfig()
case err != nil:
log.Fatal(err)
@@ -113,19 +77,27 @@ func loadConfig() config {
}
}
func mustNewKey() wgcfg.PrivateKey {
key, err := wgcfg.NewPrivateKey()
if err != nil {
log.Fatal(err)
}
return key
}
func writeNewConfig() config {
k := key.NewNode()
key := mustNewKey()
if err := os.MkdirAll(filepath.Dir(*configPath), 0777); err != nil {
log.Fatal(err)
}
cfg := config{
PrivateKey: k,
PrivateKey: key,
}
b, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
log.Fatal(err)
}
if err := atomicfile.WriteFile(*configPath, b, 0600); err != nil {
if err := atomicfile.WriteFile(*configPath, b, 0666); err != nil {
log.Fatal(err)
}
return cfg
@@ -141,11 +113,6 @@ func main() {
tsweb.DevMode = true
}
listenHost, _, err := net.SplitHostPort(*addr)
if err != nil {
log.Fatalf("invalid server address: %v", err)
}
var logPol *logpolicy.Policy
if *logCollection != "" {
logPol = logpolicy.New(*logCollection)
@@ -154,10 +121,9 @@ func main() {
cfg := loadConfig()
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
letsEncrypt := tsweb.IsProd443(*addr)
s := derp.NewServer(cfg.PrivateKey, log.Printf)
s.SetVerifyClient(*verifyClients)
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
@@ -176,13 +142,9 @@ func main() {
}
expvar.Publish("derp", s.ExpVar())
mux := http.NewServeMux()
derpHandler := derphttp.Handler(s)
derpHandler = addWebSocketSupport(s, derpHandler)
mux.Handle("/derp", derpHandler)
mux.HandleFunc("/derp/probe", probeHandler)
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
// Create our own mux so we don't expose /debug/ stuff to the world.
mux := tsweb.NewMux(debugHandler(s))
mux.Handle("/derp", derphttp.Handler(s))
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
@@ -191,7 +153,7 @@ func main() {
<p>
This is a
<a href="https://tailscale.com/">Tailscale</a>
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
<a href="https://godoc.org/tailscale.com/derp">DERP</a>
server.
</p>
`)
@@ -199,110 +161,50 @@ func main() {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
}
}))
debug := tsweb.Debugger(mux)
debug.KV("TLS hostname", *hostname)
debug.KV("Mesh key", s.HasMeshKey())
debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
}))
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
if *runSTUN {
go serveSTUN(listenHost, *stunPort)
go serveSTUN()
}
httpsrv := &http.Server{
Addr: *addr,
Handler: mux,
// Set read/write timeout. For derper, this basically
// only affects TLS setup, as read/write deadlines are
// cleared on Hijack, which the DERP server does. But
// without this, we slowly accumulate stuck TLS
// handshake goroutines forever. This also affects
// /debug/ traffic, but 30 seconds is plenty for
// Prometheus/etc scraping.
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
if serveTLS {
var err error
if letsEncrypt {
if *certDir == "" {
log.Fatalf("missing required --certdir flag")
}
log.Printf("derper: serving on %s with TLS", *addr)
var certManager certProvider
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname)
if err != nil {
log.Fatalf("derper: can not start cert provider: %v", err)
certManager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(*hostname),
Cache: autocert.DirCache(*certDir),
}
if *hostname == "derp.tailscale.com" {
certManager.HostPolicy = prodAutocertHostPolicy
certManager.Email = "security@tailscale.com"
}
httpsrv.TLSConfig = certManager.TLSConfig()
getCert := httpsrv.TLSConfig.GetCertificate
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCert(hi)
cert, err := letsEncryptGetCert(hi)
if err != nil {
return nil, err
}
cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil
}
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
httpsrv.TLSConfig.MinVersion = tls.VersionTLS12
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
label := "unknown"
switch r.TLS.Version {
case tls.VersionTLS10:
label = "1.0"
case tls.VersionTLS11:
label = "1.1"
case tls.VersionTLS12:
label = "1.2"
case tls.VersionTLS13:
label = "1.3"
go func() {
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
tlsRequestVersion.Add(label, 1)
tlsActiveVersion.Add(label, 1)
defer tlsActiveVersion.Add(label, -1)
}
// Set HTTP headers to appease automated security scanners.
//
// Security automation gets cranky when HTTPS sites don't
// set HSTS, and when they don't specify a content
// security policy for XSS mitigation.
//
// DERP's HTTP interface is only ever used for debug
// access (for which trivial safe policies work just
// fine), and by DERP clients which don't obey any of
// these browser-centric headers anyway.
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
if *httpPort > -1 {
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
}
err := port80srv.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
}
}()
}
err = rateLimitedListenAndServeTLS(httpsrv)
}()
err = httpsrv.ListenAndServeTLS("", "")
} else {
log.Printf("derper: serving on %s", *addr)
err = httpsrv.ListenAndServe()
@@ -312,44 +214,78 @@ func main() {
}
}
// probeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func probeHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "HEAD", "GET":
w.Header().Set("Access-Control-Allow-Origin", "*")
default:
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
}
func debugHandler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/debug/check" {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
return
}
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
f(`<html><body>
<h1>DERP debug</h1>
<ul>
`)
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.LONG))
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
<li><a href="/debug/pprof/">/debug/pprof/</a></li>
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine</a> (collapsed)</li>
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine</a> (full)</li>
<li><a href="/debug/check">/debug/check</a> internal consistency check</li>
<ul>
</html>
`)
})
}
func serveSTUN(host string, port int) {
pc, err := net.ListenPacket("udp", net.JoinHostPort(host, fmt.Sprint(port)))
func serveSTUN() {
pc, err := net.ListenPacket("udp", ":3478")
if err != nil {
log.Fatalf("failed to open STUN listener: %v", err)
}
log.Printf("running STUN server on %v", pc.LocalAddr())
serverSTUNListener(context.Background(), pc.(*net.UDPConn))
}
func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
var buf [64 << 10]byte
var (
n int
ua *net.UDPAddr
err error
stats = new(metrics.Set)
stunDisposition = &metrics.LabelMap{Label: "disposition"}
stunAddrFamily = &metrics.LabelMap{Label: "family"}
stunReadError = stunDisposition.Get("read_error")
stunNotSTUN = stunDisposition.Get("not_stun")
stunWriteError = stunDisposition.Get("write_error")
stunSuccess = stunDisposition.Get("success")
stunIPv4 = stunAddrFamily.Get("ipv4")
stunIPv6 = stunAddrFamily.Get("ipv6")
)
stats.Set("counter_requests", stunDisposition)
stats.Set("counter_addrfamily", stunAddrFamily)
expvar.Publish("stun", stats)
var buf [64 << 10]byte
for {
n, ua, err = pc.ReadFromUDP(buf[:])
n, addr, err := pc.ReadFrom(buf[:])
if err != nil {
if ctx.Err() != nil {
return
}
log.Printf("STUN ReadFrom: %v", err)
time.Sleep(time.Second)
stunReadError.Add(1)
continue
}
ua, ok := addr.(*net.UDPAddr)
if !ok {
log.Printf("STUN unexpected address %T %v", addr, addr)
stunReadError.Add(1)
continue
}
pkt := buf[:n]
if !stun.Is(pkt) {
stunNotSTUN.Add(1)
@@ -366,7 +302,7 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
stunIPv6.Add(1)
}
res := stun.Response(txid, ua.IP, uint16(ua.Port))
_, err = pc.WriteTo(res, ua)
_, err = pc.WriteTo(res, addr)
if err != nil {
stunWriteError.Add(1)
} else {
@@ -396,63 +332,3 @@ func defaultMeshPSKFile() string {
}
return ""
}
func rateLimitedListenAndServeTLS(srv *http.Server) error {
addr := srv.Addr
if addr == "" {
addr = ":https"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
rln := newRateLimitedListener(ln, rate.Limit(*acceptConnLimit), *acceptConnBurst)
expvar.Publish("tls_listener", rln.ExpVar())
defer rln.Close()
return srv.ServeTLS(rln, "", "")
}
type rateLimitedListener struct {
// These are at the start of the struct to ensure 64-bit alignment
// on 32-bit architecture regardless of what other fields may exist
// in this package.
numAccepts expvar.Int // does not include number of rejects
numRejects expvar.Int
net.Listener
lim *rate.Limiter
}
func newRateLimitedListener(ln net.Listener, limit rate.Limit, burst int) *rateLimitedListener {
return &rateLimitedListener{Listener: ln, lim: rate.NewLimiter(limit, burst)}
}
func (l *rateLimitedListener) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_accepted_connections", &l.numAccepts)
m.Set("counter_rejected_connections", &l.numRejects)
return m
}
var errLimitedConn = errors.New("cannot accept connection; rate limited")
func (l *rateLimitedListener) Accept() (net.Conn, error) {
// Even under a rate limited situation, we accept the connection immediately
// and close it, rather than being slow at accepting new connections.
// This provides two benefits: 1) it signals to the client that something
// is going on on the server, and 2) it prevents new connections from
// piling up and occupying resources in the OS kernel.
// The client will retry as needing (with backoffs in place).
cn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
if !l.lim.Allow() {
l.numRejects.Add(1)
cn.Close()
return nil, errLimitedConn
}
l.numAccepts.Add(1)
return cn, nil
}

View File

@@ -6,10 +6,7 @@ package main
import (
"context"
"net"
"testing"
"tailscale.com/net/stun"
)
func TestProdAutocertHostPolicy(t *testing.T) {
@@ -34,36 +31,5 @@ func TestProdAutocertHostPolicy(t *testing.T) {
t.Errorf("f(%q) = %v; want %v", tt.in, got, tt.wantOK)
}
}
}
func BenchmarkServerSTUN(b *testing.B) {
b.ReportAllocs()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer pc.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go serverSTUNListener(ctx, pc.(*net.UDPConn))
addr := pc.LocalAddr().(*net.UDPAddr)
var resBuf [1500]byte
cc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
b.Fatal(err)
}
tx := stun.NewTxID()
req := stun.Request(tx)
for i := 0; i < b.N; i++ {
if _, err := cc.WriteToUDP(req, addr); err != nil {
b.Fatal(err)
}
_, _, err := cc.ReadFromUDP(resBuf[:])
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -5,13 +5,10 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -41,36 +38,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
return err
}
c.MeshKey = s.MeshKey()
// For meshed peers within a region, connect via VPC addresses.
c.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
var d net.Dialer
var r net.Resolver
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
base := strings.TrimSuffix(host, ".tailscale.com")
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
vpcHost := base + "-vpc.tailscale.com"
ips, _ := r.LookupIP(subCtx, "ip", vpcHost)
if len(ips) > 0 {
vpcAddr := net.JoinHostPort(ips[0].String(), port)
c, err := d.DialContext(subCtx, network, vpcAddr)
if err == nil {
log.Printf("connected to %v (%v) instead of %v", vpcHost, ips[0], base)
return c, nil
}
log.Printf("failed to connect to %v (%v): %v; trying non-VPC route", vpcHost, ips[0], err)
}
}
return d.DialContext(ctx, network, addr)
})
add := func(k key.NodePublic) { s.AddPacketForwarder(k, c) }
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
return nil
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"expvar"
"log"
"net/http"
"strings"
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/derp/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
// addWebSocketSupport returns a Handle wrapping base that adds WebSocket server support.
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
up := strings.ToLower(r.Header.Get("Upgrade"))
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
base.ServeHTTP(w, r)
return
}
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
})
if err != nil {
log.Printf("websocket.Accept: %v", err)
return
}
defer c.Close(websocket.StatusInternalError, "closing")
if c.Subprotocol() != "derp" {
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
return
}
counterWebSocketAccepts.Add(1)
wc := wsconn.New(c)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(wc, brw, r.RemoteAddr)
})
}

View File

@@ -1,556 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The derpprobe binary probes derpers.
package main // import "tailscale.com/cmd/derper/derpprobe"
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/x509"
"encoding/json"
"errors"
"flag"
"fmt"
"html"
"io"
"log"
"net"
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/net/stun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
)
// certReissueAfter is the time after which we expect all certs to be
// reissued, at minimum.
//
// This is currently set to the date of the LetsEncrypt ALPN revocation event of Jan 2022:
// https://community.letsencrypt.org/t/questions-about-renewing-before-tls-alpn-01-revocations/170449
//
// If there's another revocation event, bump this again.
var certReissueAfter = time.Unix(1643226768, 0)
var (
mu sync.Mutex
state = map[nodePair]pairStatus{}
lastDERPMap *tailcfg.DERPMap
lastDERPMapAt time.Time
certs = map[string]*x509.Certificate{}
)
func main() {
flag.Parse()
// proactively load the DERP map. Nothing terrible happens if this fails, so we ignore
// the error. The Slack bot will print a notification that the DERP map was empty.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, _ = getDERPMap(ctx)
go probeLoop()
go slackLoop()
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
}
func setCert(name string, cert *x509.Certificate) {
mu.Lock()
defer mu.Unlock()
certs[name] = cert
}
type overallStatus struct {
good, bad []string
}
func (st *overallStatus) addBadf(format string, a ...any) {
st.bad = append(st.bad, fmt.Sprintf(format, a...))
}
func (st *overallStatus) addGoodf(format string, a ...any) {
st.good = append(st.good, fmt.Sprintf(format, a...))
}
func getOverallStatus() (o overallStatus) {
mu.Lock()
defer mu.Unlock()
if lastDERPMap == nil {
o.addBadf("no DERP map")
return
}
now := time.Now()
if age := now.Sub(lastDERPMapAt); age > time.Minute {
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
}
addPairMeta := func(pair nodePair) {
st, ok := state[pair]
age := now.Sub(st.at).Round(time.Second)
switch {
case !ok:
o.addBadf("no state for %v", pair)
case st.err != nil:
o.addBadf("%v: %v", pair, st.err)
case age > 90*time.Second:
o.addBadf("%v: update is %v old", pair, age)
default:
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
}
}
for _, reg := range sortedRegions(lastDERPMap) {
for _, from := range reg.Nodes {
addPairMeta(nodePair{"UDP", from.Name})
for _, to := range reg.Nodes {
addPairMeta(nodePair{from.Name, to.Name})
}
}
}
var subjs []string
for k := range certs {
subjs = append(subjs, k)
}
sort.Strings(subjs)
soon := time.Now().Add(14 * 24 * time.Hour) // in 2 weeks; autocert does 30 days by default
for _, s := range subjs {
cert := certs[s]
if cert.NotBefore.Before(certReissueAfter) {
o.addBadf("cert %q needs reissuing; NotBefore=%v", s, cert.NotBefore.Format(time.RFC3339))
continue
}
if cert.NotAfter.Before(soon) {
o.addBadf("cert %q expiring soon (%v); wasn't auto-refreshed", s, cert.NotAfter.Format(time.RFC3339))
continue
}
o.addGoodf("cert %q good %v - %v", s, cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339))
}
return
}
func serve(w http.ResponseWriter, r *http.Request) {
st := getOverallStatus()
summary := "All good"
if (float64(len(st.bad)) / float64(len(st.bad)+len(st.good))) > 0.25 {
// This will generate an alert and page a human.
// It also ends up in Slack, but as part of the alert handling pipeline not
// because we generated a Slack notification from here.
w.WriteHeader(500)
summary = fmt.Sprintf("%d problems", len(st.bad))
}
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
for _, s := range st.bad {
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
}
for _, s := range st.good {
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
}
io.WriteString(w, "</ul></body></html>\n")
}
func notifySlack(text string) error {
type SlackRequestBody struct {
Text string `json:"text"`
}
slackBody, err := json.Marshal(SlackRequestBody{Text: text})
if err != nil {
return err
}
webhookUrl := os.Getenv("SLACK_WEBHOOK")
if webhookUrl == "" {
return errors.New("No SLACK_WEBHOOK configured")
}
req, err := http.NewRequest("POST", webhookUrl, bytes.NewReader(slackBody))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New(resp.Status)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != "ok" {
return errors.New("Non-ok response returned from Slack")
}
return nil
}
// We only page a human if it looks like there is a significant outage across multiple regions.
// To Slack, we report all failures great and small.
func slackLoop() {
inBadState := false
for {
time.Sleep(time.Second * 30)
st := getOverallStatus()
if len(st.bad) > 0 && !inBadState {
err := notifySlack(strings.Join(st.bad, "\n"))
if err == nil {
inBadState = true
} else {
log.Printf("%d problems, notify Slack failed: %v", len(st.bad), err)
}
}
if len(st.bad) == 0 && inBadState {
err := notifySlack("All DERPs recovered.")
if err == nil {
inBadState = false
}
}
}
}
func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
ret := make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, r := range dm.Regions {
ret = append(ret, r)
}
sort.Slice(ret, func(i, j int) bool { return ret[i].RegionID < ret[j].RegionID })
return ret
}
type nodePair struct {
from string // DERPNode.Name, or "UDP" for a STUN query to 'to'
to string // DERPNode.Name
}
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
type pairStatus struct {
err error
latency time.Duration
at time.Time
}
func setDERPMap(dm *tailcfg.DERPMap) {
mu.Lock()
defer mu.Unlock()
lastDERPMap = dm
lastDERPMapAt = time.Now()
}
func setState(p nodePair, latency time.Duration, err error) {
mu.Lock()
defer mu.Unlock()
st := pairStatus{
err: err,
latency: latency,
at: time.Now(),
}
state[p] = st
if err != nil {
log.Printf("%+v error: %v", p, err)
} else {
log.Printf("%+v: %v", p, latency.Round(time.Millisecond))
}
}
func probeLoop() {
ticker := time.NewTicker(15 * time.Second)
for {
err := probe()
if err != nil {
log.Printf("probe: %v", err)
}
<-ticker.C
}
}
func probe() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
dm, err := getDERPMap(ctx)
if err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(len(dm.Regions))
for _, reg := range dm.Regions {
reg := reg
go func() {
defer wg.Done()
for _, from := range reg.Nodes {
latency, err := probeUDP(ctx, dm, from)
setState(nodePair{"UDP", from.Name}, latency, err)
for _, to := range reg.Nodes {
latency, err := probeNodePair(ctx, dm, from, to)
setState(nodePair{from.Name, to.Name}, latency, err)
}
}
}()
}
wg.Wait()
return ctx.Err()
}
func probeUDP(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (latency time.Duration, err error) {
pc, err := net.ListenPacket("udp", ":0")
if err != nil {
return 0, err
}
defer pc.Close()
uc := pc.(*net.UDPConn)
tx := stun.NewTxID()
req := stun.Request(tx)
for _, ipStr := range []string{n.IPv4, n.IPv6} {
if ipStr == "" {
continue
}
port := n.STUNPort
if port == -1 {
continue
}
if port == 0 {
port = 3478
}
for {
ip := net.ParseIP(ipStr)
_, err := uc.WriteToUDP(req, &net.UDPAddr{IP: ip, Port: port})
if err != nil {
return 0, err
}
buf := make([]byte, 1500)
uc.SetReadDeadline(time.Now().Add(2 * time.Second))
t0 := time.Now()
n, _, err := uc.ReadFromUDP(buf)
d := time.Since(t0)
if err != nil {
if ctx.Err() != nil {
return 0, fmt.Errorf("timeout reading from %v: %v", ip, err)
}
if d < time.Second {
return 0, fmt.Errorf("error reading from %v: %v", ip, err)
}
time.Sleep(100 * time.Millisecond)
continue
}
txBack, _, _, err := stun.ParseResponse(buf[:n])
if err != nil {
return 0, fmt.Errorf("parsing STUN response from %v: %v", ip, err)
}
if txBack != tx {
return 0, fmt.Errorf("read wrong tx back from %v", ip)
}
if latency == 0 || d < latency {
latency = d
}
break
}
}
return latency, nil
}
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
// The passed in context is a minute for the whole region. The
// idea is that each node pair in the region will be done
// serially and regularly in the future, reusing connections
// (at least in the happy path). For now they don't reuse
// connections and probe at most once every 15 seconds. We
// bound the duration of a single node pair within a region
// so one bad one can't starve others.
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
fromc, err := newConn(ctx, dm, from)
if err != nil {
return 0, err
}
defer fromc.Close()
toc, err := newConn(ctx, dm, to)
if err != nil {
return 0, err
}
defer toc.Close()
// Wait a bit for from's node to hear about to existing on the
// other node in the region, in the case where the two nodes
// are different.
if from.Name != to.Name {
time.Sleep(100 * time.Millisecond) // pretty arbitrary
}
// Make a random packet
pkt := make([]byte, 8)
crand.Read(pkt)
t0 := time.Now()
// Send the random packet.
sendc := make(chan error, 1)
go func() {
sendc <- fromc.Send(toc.SelfPublicKey(), pkt)
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout sending via %q: %w", from.Name, ctx.Err())
case err := <-sendc:
if err != nil {
return 0, fmt.Errorf("error sending via %q: %w", from.Name, err)
}
}
// Receive the random packet.
recvc := make(chan any, 1) // either derp.ReceivedPacket or error
go func() {
for {
m, err := toc.Recv()
if err != nil {
recvc <- err
return
}
switch v := m.(type) {
case derp.ReceivedPacket:
recvc <- v
default:
log.Printf("%v: ignoring Recv frame type %T", to.Name, v)
// Loop.
}
}
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout receiving from %q: %w", to.Name, ctx.Err())
case v := <-recvc:
if err, ok := v.(error); ok {
return 0, fmt.Errorf("error receiving from %q: %w", to.Name, err)
}
p := v.(derp.ReceivedPacket)
if p.Source != fromc.SelfPublicKey() {
return 0, fmt.Errorf("got data packet from unexpected source, %v", p.Source)
}
if !bytes.Equal(p.Data, pkt) {
return 0, fmt.Errorf("unexpected data packet %q", p.Data)
}
}
return time.Since(t0), nil
}
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
priv := key.NewNode()
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
rid := n.RegionID
return &tailcfg.DERPRegion{
RegionID: rid,
RegionCode: fmt.Sprintf("%s-%s", dm.Regions[rid].RegionCode, n.Name),
RegionName: dm.Regions[rid].RegionName,
Nodes: []*tailcfg.DERPNode{n},
}
})
dc.IsProber = true
err := dc.Connect(ctx)
if err != nil {
return nil, err
}
cs, ok := dc.TLSConnectionState()
if !ok {
dc.Close()
return nil, errors.New("no TLS state")
}
if len(cs.PeerCertificates) == 0 {
dc.Close()
return nil, errors.New("no peer certificates")
}
if cs.ServerName != n.HostName {
dc.Close()
return nil, fmt.Errorf("TLS server name %q != derp hostname %q", cs.ServerName, n.HostName)
}
setCert(cs.ServerName, cs.PeerCertificates[0])
errc := make(chan error, 1)
go func() {
m, err := dc.Recv()
if err != nil {
errc <- err
return
}
switch m.(type) {
case derp.ServerInfoMessage:
errc <- nil
default:
errc <- fmt.Errorf("unexpected first message type %T", errc)
}
}()
select {
case err := <-errc:
if err != nil {
go dc.Close()
return nil, err
}
case <-ctx.Done():
go dc.Close()
return nil, fmt.Errorf("timeout waiting for ServerInfoMessage: %w", ctx.Err())
}
return dc, nil
}
var httpOrFileClient = &http.Client{Transport: httpOrFileTransport()}
func httpOrFileTransport() http.RoundTripper {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
return tr
}
func getDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", *derpMapURL, nil)
if err != nil {
return nil, err
}
res, err := httpOrFileClient.Do(req)
if err != nil {
mu.Lock()
defer mu.Unlock()
if lastDERPMap != nil && time.Since(lastDERPMapAt) < 10*time.Minute {
// Assume that control is restarting and use
// the same one for a bit.
return lastDERPMap, nil
}
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetching %s: %s", *derpMapURL, res.Status)
}
dm := new(tailcfg.DERPMap)
if err := json.NewDecoder(res.Body).Decode(dm); err != nil {
return nil, fmt.Errorf("decoding %s JSON: %v", *derpMapURL, err)
}
setDERPMap(dm)
return dm, nil
}

View File

@@ -1,211 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The hello binary runs hello.ts.net.
package main // import "tailscale.com/cmd/hello"
import (
"context"
"crypto/tls"
_ "embed"
"encoding/json"
"errors"
"flag"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
)
var (
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
)
//go:embed hello.tmpl.html
var embeddedTemplate string
func main() {
flag.Parse()
if *testIP != "" {
res, err := tailscale.WhoIs(context.Background(), *testIP)
if err != nil {
log.Fatal(err)
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
e.Encode(res)
return
}
if devMode() {
// Parse it optimistically
var err error
tmpl, err = template.New("home").Parse(embeddedTemplate)
if err != nil {
log.Printf("ignoring template error in dev mode: %v", err)
}
} else {
if embeddedTemplate == "" {
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
}
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
}
http.HandleFunc("/", root)
log.Printf("Starting hello server.")
errc := make(chan error, 1)
if *httpAddr != "" {
log.Printf("running HTTP server on %s", *httpAddr)
go func() {
errc <- http.ListenAndServe(*httpAddr, nil)
}()
}
if *httpsAddr != "" {
log.Printf("running HTTPS server on %s", *httpsAddr)
go func() {
hs := &http.Server{
Addr: *httpsAddr,
TLSConfig: &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
switch hi.ServerName {
case "hello.ts.net":
return tailscale.GetCertificate(hi)
case "hello.ipn.dev":
c, err := tls.LoadX509KeyPair(
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
)
if err != nil {
return nil, err
}
return &c, nil
}
return nil, errors.New("invalid SNI name")
},
},
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 20 * time.Second,
MaxHeaderBytes: 10 << 10,
}
errc <- hs.ListenAndServeTLS("", "")
}()
}
log.Fatal(<-errc)
}
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := ioutil.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
}
return template.New("home").Parse(string(tmplData))
}
return tmpl, nil
}
// tmpl is the template used in prod mode.
// In dev mode it's only used if the template file doesn't exist on disk.
// It's initialized by main after flag parsing.
var tmpl *template.Template
type tmplData struct {
DisplayName string // "Foo Barberson"
LoginName string // "foo@bar.com"
ProfilePicURL string // "https://..."
MachineName string // "imac5k"
MachineOS string // "Linux"
IP string // "100.2.3.4"
}
func tailscaleIP(who *apitype.WhoIsResponse) string {
if who == nil {
return ""
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP().String()
}
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IsSingleIP() {
return nodeIP.IP().String()
}
}
return ""
}
func root(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && *httpsAddr != "" {
host := r.Host
if strings.Contains(r.Host, "100.101.102.103") ||
strings.Contains(r.Host, "hello.ipn.dev") {
host = "hello.ts.net"
}
http.Redirect(w, r, "https://"+host, http.StatusFound)
return
}
if r.RequestURI != "/" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
return
}
tmpl, err := getTmpl()
if err != nil {
w.Header().Set("Content-Type", "text/plain")
http.Error(w, "template error: "+err.Error(), 500)
return
}
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
var data tmplData
if err != nil {
if devMode() {
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
data = tmplData{
DisplayName: "Taily Scalerson",
LoginName: "taily@scaler.son",
ProfilePicURL: "https://placekitten.com/200/200",
MachineName: "scaled",
MachineOS: "Linux",
IP: "100.1.2.3",
}
} else {
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
return
}
} else {
data = tmplData{
DisplayName: who.UserProfile.DisplayName,
LoginName: who.UserProfile.LoginName,
ProfilePicURL: who.UserProfile.ProfilePicURL,
MachineName: firstLabel(who.Node.ComputedName),
MachineOS: who.Node.Hostinfo.OS(),
IP: tailscaleIP(who),
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
// firstLabel s up until the first period, if any.
func firstLabel(s string) string {
s, _, _ = strings.Cut(s, ".")
return s
}

View File

@@ -1,436 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Hello from Tailscale</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body,
main {
height: 100%;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #dad6d5;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
font-size: 1rem;
font-weight: inherit;
}
a {
color: inherit;
}
p {
margin: 0;
}
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 24rem;
width: 95%;
margin-left: auto;
margin-right: auto;
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.pl-3 {
padding-left: 0.75rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.pt-4 {
padding-top: 1rem;
}
.mr-2 {
margin-right: 0.5rem;
;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mb-12 {
margin-bottom: 3rem;
}
.width-full {
width: 100%;
}
.min-width-0 {
min-width: 0;
}
.rounded-lg {
border-radius: 0.5rem;
}
.relative {
position: relative;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.border {
border-width: 1px;
}
.border-t-1 {
border-top-width: 1px;
}
.border-gray-100 {
border-color: #f7f5f4;
}
.border-gray-200 {
border-color: #eeebea;
}
.border-gray-300 {
border-color: #dad6d5;
}
.bg-white {
background-color: white;
}
.bg-gray-0 {
background-color: #faf9f8;
}
.bg-gray-100 {
background-color: #f7f5f4;
}
.text-green-600 {
color: #0d4b3b;
}
.text-blue-600 {
color: #3f5db3;
}
.hover\:text-blue-800:hover {
color: #253570;
}
.text-gray-600 {
color: #444342;
}
.text-gray-700 {
color: #2e2d2d;
}
.text-gray-800 {
color: #232222;
}
.text-center {
text-align: center;
}
.text-sm {
font-size: 0.875rem;
}
.font-title {
font-size: 1.25rem;
letter-spacing: -0.025em;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.font-regular {
font-weight: 400;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.profile-pic {
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
background-size: cover;
margin-right: 0.5rem;
flex-shrink: 0;
}
.panel {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animate .panel {
transform: translateY(10%);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
}
.animate .panel-interior {
opacity: 0.0;
transition: opacity 1200ms ease;
}
.animate .logo {
transform: translateY(2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-title {
transform: translateY(1.6rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-text {
transform: translateY(1.2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .footer {
transform: translateY(-0.5rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animating .panel {
transform: translateY(0);
opacity: 1.0;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animating .panel-interior {
opacity: 1.0;
}
.animating .spinner {
opacity: 0.0;
}
.animating .logo,
.animating .header-title,
.animating .header-text,
.animating .footer {
transform: translateY(0);
opacity: 1.0;
}
.spinner {
display: inline-flex;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
align-items: center;
transition: opacity 200ms ease;
}
.spinner span {
display: inline-block;
background-color: currentColor;
border-radius: 9999px;
animation-name: loading-dots-blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 0.35em;
height: 0.35em;
margin: 0 0.15em;
}
.spinner span:nth-child(2) {
animation-delay: 200ms;
}
.spinner span:nth-child(3) {
animation-delay: 400ms;
}
.spinner {
display: none;
}
.animate .spinner {
display: inline-flex;
}
@keyframes loading-dots-blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion) {
* {
animation-duration: 0ms !important;
transition-duration: 0ms !important;
transition-delay: 0ms !important;
}
}
</style>
</head>
<body class="bg-gray-100">
<script>
(function() {
var lastSeen = localStorage.getItem("lastSeen");
if (!lastSeen) {
document.body.classList.add("animate");
window.addEventListener("load", function () {
setTimeout(function () {
document.body.classList.add("animating");
localStorage.setItem("lastSeen", Date.now());
}, 100);
});
}
})();
</script>
<main class="text-gray-800">
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
</svg>
<header class="mb-8 text-center">
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
<p class="header-text">This device is signed in as…</p>
</header>
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
<div class="spinner text-gray-600">
<span></span>
<span></span>
<span></span>
</div>
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
<div class="overflow-hidden">
{{ with .DisplayName }}
<h4 class="font-semibold truncate">{{.}}</h4>
{{ end }}
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
</div>
</div>
<div
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
<div class="flex items-center min-width-0">
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
</div>
<h5>{{.IP}}</h5>
</div>
</div>
<footer class="footer text-gray-600 text-center mb-12">
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
target="_blank">what you can do next &rarr;</a></p>
</footer>
</main>
</body>
</html>

View File

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

View File

@@ -21,9 +21,6 @@ import (
// into a map of filePathOnDisk -> filePathInPackage.
func parseFiles(s string) (map[string]string, error) {
ret := map[string]string{}
if len(s) == 0 {
return ret, nil
}
for _, f := range strings.Split(s, ",") {
fs := strings.Split(f, ":")
if len(fs) != 2 {

View File

@@ -1,46 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The printdep command is a build system tool for printing out information
// about dependencies.
package main
import (
"flag"
"fmt"
"log"
"runtime"
"strings"
ts "tailscale.com"
)
var (
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
)
func main() {
flag.Parse()
if *goToolchain {
fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
}
if *goToolchainURL {
var suffix string
switch runtime.GOARCH {
case "amd64":
// None
case "arm64":
suffix = "-" + runtime.GOARCH
default:
log.Fatalf("unsupported GOARCH %q", runtime.GOARCH)
}
switch runtime.GOOS {
case "linux", "darwin":
default:
log.Fatalf("unsupported GOOS %q", runtime.GOOS)
}
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
}
}

View File

@@ -1,121 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program speedtest provides the speedtest command. The reason to keep it separate from
// the normal tailscale cli is because it is not yet ready to go in the tailscale binary.
// It will be included in the tailscale cli after it has been added to tailscaled.
// Example usage for client command: go run cmd/speedtest -host 127.0.0.1:20333 -t 5s
// This will connect to the server on 127.0.0.1:20333 and start a 5 second download speedtest.
// Example usage for server command: go run cmd/speedtest -s -host :20333
// This will start a speedtest server on port 20333.
package main
import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"strconv"
"text/tabwriter"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/net/speedtest"
)
// Runs the speedtest command as a commandline program
func main() {
args := os.Args[1:]
if err := speedtestCmd.Parse(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
err := speedtestCmd.Run(context.Background())
if errors.Is(err, flag.ErrHelp) {
fmt.Fprintln(os.Stderr, speedtestCmd.ShortUsage)
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
// speedtestCmd is the root command. It runs either the server or client depending on the
// flags passed to it.
var speedtestCmd = &ffcli.Command{
Name: "speedtest",
ShortUsage: "speedtest [-host <host:port>] [-s] [-r] [-t <test duration>]",
ShortHelp: "Run a speed test",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("speedtest", flag.ExitOnError)
fs.StringVar(&speedtestArgs.host, "host", ":20333", "host:port pair to connect to or listen on")
fs.DurationVar(&speedtestArgs.testDuration, "t", speedtest.DefaultDuration, "duration of the speed test")
fs.BoolVar(&speedtestArgs.runServer, "s", false, "run a speedtest server")
fs.BoolVar(&speedtestArgs.reverse, "r", false, "run in reverse mode (server sends, client receives)")
return fs
})(),
Exec: runSpeedtest,
}
var speedtestArgs struct {
host string
testDuration time.Duration
runServer bool
reverse bool
}
func runSpeedtest(ctx context.Context, args []string) error {
if _, _, err := net.SplitHostPort(speedtestArgs.host); err != nil {
var addrErr *net.AddrError
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
// if no port is provided, append the default port
speedtestArgs.host = net.JoinHostPort(speedtestArgs.host, strconv.Itoa(speedtest.DefaultPort))
}
}
if speedtestArgs.runServer {
listener, err := net.Listen("tcp", speedtestArgs.host)
if err != nil {
return err
}
fmt.Printf("listening on %v\n", listener.Addr())
return speedtest.Serve(listener)
}
// Ensure the duration is within the allowed range
if speedtestArgs.testDuration < speedtest.MinDuration || speedtestArgs.testDuration > speedtest.MaxDuration {
return fmt.Errorf("test duration must be within %v and %v", speedtest.MinDuration, speedtest.MaxDuration)
}
dir := speedtest.Download
if speedtestArgs.reverse {
dir = speedtest.Upload
}
fmt.Printf("Starting a %s test with %s\n", dir, speedtestArgs.host)
results, err := speedtest.RunClient(dir, speedtestArgs.testDuration, speedtestArgs.host)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
fmt.Println("Results:")
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
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())
}
w.Flush()
return nil
}

View File

@@ -1,231 +0,0 @@
// 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.
// Admin commands.
package cli
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var adminCmd = &ffcli.Command{
Name: "admin",
Exec: runAdmin,
LongHelp: `"tailscale admin" contains admin commands to manage a Tailscale network.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("admin")
fs.StringVar(&adminArgs.apiBase, "api-server", "https://api.tailscale.com", "which Tailscale server instance to use. Ignored when --token-file is empty.")
fs.StringVar(&adminArgs.tokenFile, "token-file", "", "if non-empty, filename containing API token to use. If empty, authentication is done via the active Tailscale control plane connection.")
fs.StringVar(&adminArgs.tailnet, "tailnet", "", "Tailnet to query or edit. Required if token-file is used. Must be blank if token-file is blank, in which case the tailnet used is the same as the active tailnet.")
return fs
})(),
Subcommands: []*ffcli.Command{
newTailnetACLGetCmd(),
newTailnetDeviceListCmd(),
newTailnetKeyListCmd(),
},
}
var adminArgs struct {
tokenFile string
tailnet string
apiBase string
}
func runAdmin(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown command; see 'tailscale admin --help'")
}
return errors.New("see 'tailscale admin --help'")
}
type adminClient struct {
apiBase string // e.g. "https://api.tailscale.com"
token string // non-empty if using token-based auth
hc *http.Client
tailnet string // always non-empty
}
func getAdminHTTPClient() (*adminClient, error) {
tokenFile := adminArgs.tokenFile
tailnet := adminArgs.tailnet
apiBase := adminArgs.apiBase
if (tokenFile != "") != (tailnet != "") {
return nil, errors.New("--token-file and --tailnet must both be blank or both be specified")
}
if tailnet == "" {
st, err := tailscale.StatusWithoutPeers(context.Background())
if err != nil {
return nil, err
}
if st.BackendState != "Running" {
return nil, fmt.Errorf("Tailscale must be running; currently in state %q", st.BackendState)
}
if st.CurrentTailnet == nil {
return nil, fmt.Errorf("no CurrentTailnet in status")
}
tailnet = st.CurrentTailnet.Name
// TODO(bradfitz): put apiBase in *ipnstate.TailnetStatus? update apiBase here?
}
ac := &adminClient{
tailnet: tailnet,
apiBase: apiBase,
}
if tokenFile != "" {
v, err := os.ReadFile(tokenFile)
if err != nil {
return nil, err
}
token := strings.TrimSpace(string(v))
if token == "" || strings.Contains(token, "\n") {
return nil, fmt.Errorf("expect exactly 1 line in API token file %v", tokenFile)
}
ac.token = token
ac.hc = http.DefaultClient
} else {
// Otherwise, proxy via the local tailscaled and use its identity.
ac.hc = &http.Client{Transport: apiViaTailscaledTransport{}}
ac.apiBase = "http://local-tailscaled.sock"
}
return ac, nil
}
func newTailnetDeviceListCmd() *ffcli.Command {
var fields string
const sub = "tailnet-device-list"
fs := newFlagSet(sub)
fs.StringVar(&fields, "fields", "default", "comma-separated fields to include in response or 'default', 'all'")
return &ffcli.Command{
Name: sub,
ShortHelp: "list devices",
FlagSet: fs,
Exec: func(ctx context.Context, args []string) error {
ac, err := getAdminHTTPClient()
if err != nil {
return err
}
q := url.Values{"fields": []string{fields}}
return writeResJSON(ac.hc.Get(ac.apiBase + "/api/v2/tailnet/" + ac.tailnet + "/devices?" + q.Encode()))
},
}
}
func newTailnetKeyListCmd() *ffcli.Command {
const sub = "tailnet-key-list"
return &ffcli.Command{
Name: sub,
ShortHelp: "list keys or specific key (with keyID as argument)",
Exec: func(ctx context.Context, args []string) error {
var suf string
if len(args) == 1 {
suf = "/" + args[0]
} else if len(args) > 1 {
return errors.New("too many arguments")
}
ac, err := getAdminHTTPClient()
if err != nil {
return err
}
return writeResJSON(ac.hc.Get(ac.apiBase + "/api/v2/tailnet/" + ac.tailnet + "/keys" + suf))
},
}
}
func newTailnetACLGetCmd() *ffcli.Command {
var asJSON bool // true is JSON, false is HuJSON
const sub = "tailnet-acl-get"
fs := newFlagSet(sub)
fs.BoolVar(&asJSON, "json", false, "if true, return ACL is JSON format. The default of false means to use the original HuJSON JSON superset form that allows comments and trailing commas.")
return &ffcli.Command{
Name: sub,
ShortHelp: "list Tailnet ACL/config policy",
FlagSet: fs,
Exec: func(ctx context.Context, args []string) error {
ac, err := getAdminHTTPClient()
if err != nil {
return err
}
req, err := http.NewRequest("GET", ac.apiBase+"/api/v2/tailnet/"+ac.tailnet+"/acl", nil)
if err != nil {
return err
}
if asJSON {
req.Header.Set("Accept", "application/json")
}
res, err := ac.hc.Do(req)
if err != nil {
return err
}
if asJSON {
return writeResJSON(res, err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("%v: %s", res.Status, body)
}
all, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var buf bytes.Buffer
buf.Write(all)
ensureTrailingNewline(&buf)
os.Stdout.Write(buf.Bytes())
return nil
},
}
}
// apiViaTailscaledTransport is an http.RoundTripper that makes
// Tailscale API HTTP requests via the localapi to tailscaled,
// which then forwards them on over Noise.
type apiViaTailscaledTransport struct{}
func (apiViaTailscaledTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return tailscale.DoLocalRequest(r)
}
func ensureTrailingNewline(buf *bytes.Buffer) {
if buf.Len() > 0 && buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
}
func writeResJSON(res *http.Response, err error) error {
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("%v: %s", res.Status, body)
}
all, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var buf bytes.Buffer
if err := json.Indent(&buf, all, "", "\t"); err != nil {
return err
}
ensureTrailingNewline(&buf)
os.Stdout.Write(buf.Bytes())
return nil
}

View File

@@ -1,57 +0,0 @@
<html>
<head>
<title>Redirecting...</title>
<style>
html,
body {
height: 100%;
}
html {
background-color: rgb(249, 247, 246);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner {
margin-bottom: 2rem;
border: 4px rgba(112, 110, 109, 0.5) solid;
border-left-color: transparent;
border-radius: 9999px;
width: 4rem;
height: 4rem;
-webkit-animation: spin 700ms linear infinite;
animation: spin 800ms linear infinite;
}
.label {
color: rgb(112, 110, 109);
padding-left: 0.4rem;
}
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head> <body>
<div class="spinner"></div>
<div class="label">Redirecting...</div>
</body>

View File

@@ -1,37 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"errors"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
Name: "bugreport",
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
}
func runBugReport(ctx context.Context, args []string) error {
var note string
switch len(args) {
case 0:
case 1:
note = args[0]
default:
return errors.New("unknown argumets")
}
logMarker, err := tailscale.BugReport(ctx, note)
if err != nil {
return err
}
outln(logMarker)
return nil
}

View File

@@ -1,152 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"crypto/tls"
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/atomicfile"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/version"
)
var certCmd = &ffcli.Command{
Name: "cert",
Exec: runCert,
ShortHelp: "get TLS certs",
ShortUsage: "cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert")
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
return fs
})(),
}
var certArgs struct {
certFile string
keyFile string
serve bool
}
func runCert(ctx context.Context, args []string) error {
if certArgs.serve {
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: tailscale.GetCertificate,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
return
}
}
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
}),
}
log.Printf("running TLS server on :443 ...")
return s.ListenAndServeTLS("", "")
}
if len(args) != 1 {
var hint bytes.Buffer
if st, err := tailscale.Status(ctx); err == nil {
if st.BackendState != ipn.Running.String() {
fmt.Fprintf(&hint, "\nTailscale is not running.\n")
} else if len(st.CertDomains) == 0 {
fmt.Fprintf(&hint, "\nHTTPS cert support is not enabled/configured for your tailnet.\n")
} else if len(st.CertDomains) == 1 {
fmt.Fprintf(&hint, "\nFor domain, use %q.\n", st.CertDomains[0])
} else {
fmt.Fprintf(&hint, "\nValid domain options: %q.\n", st.CertDomains)
}
}
return fmt.Errorf("Usage: tailscale cert [flags] <domain>%s", hint.Bytes())
}
domain := args[0]
printf := func(format string, a ...any) {
printf(format, a...)
}
if certArgs.certFile == "-" || certArgs.keyFile == "-" {
printf = log.Printf
log.SetFlags(0)
}
if certArgs.certFile == "" && certArgs.keyFile == "" {
certArgs.certFile = domain + ".crt"
certArgs.keyFile = domain + ".key"
}
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
if err != nil {
return err
}
needMacWarning := version.IsSandboxedMacOS()
macWarn := func() {
if !needMacWarning {
return
}
needMacWarning = false
dir := "io.tailscale.ipn.macos"
if version.IsMacSysExt() {
dir = "io.tailscale.ipn.macsys"
}
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s/Data\n", dir)
}
if certArgs.certFile != "" {
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
if err != nil {
return err
}
if certArgs.certFile != "-" {
macWarn()
if certChanged {
printf("Wrote public cert to %v\n", certArgs.certFile)
} else {
printf("Public cert unchanged at %v\n", certArgs.certFile)
}
}
}
if certArgs.keyFile != "" {
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
if err != nil {
return err
}
if certArgs.keyFile != "-" {
macWarn()
if keyChanged {
printf("Wrote private key to %v\n", certArgs.keyFile)
} else {
printf("Private key unchanged at %v\n", certArgs.keyFile)
}
}
}
return nil
}
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
if filename == "-" {
Stdout.Write(contents)
return false, nil
}
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
return false, nil
}
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
return false, err
}
return true, nil
}

View File

@@ -8,241 +8,98 @@ package cli
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/syncs"
"tailscale.com/version/distro"
)
var Stderr io.Writer = os.Stderr
var Stdout io.Writer = os.Stdout
func printf(format string, a ...any) {
fmt.Fprintf(Stdout, format, a...)
}
// outln is like fmt.Println in the common case, except when Stdout is
// changed (as in js/wasm).
//
// It's not named println because that looks like the Go built-in
// which goes to stderr and formats slightly differently.
func outln(a ...any) {
fmt.Fprintln(Stdout, a...)
}
// ActLikeCLI reports whether a GUI application should act like the
// CLI based on os.Args, GOOS, the context the process is running in
// (pty, parent PID), etc.
func ActLikeCLI() bool {
// This function is only used on macOS.
if runtime.GOOS != "darwin" {
if len(os.Args) < 2 {
return false
}
// Escape hatch to let people force running the macOS
// GUI Tailscale binary as the CLI.
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v {
switch os.Args[1] {
case "up", "down", "status", "netcheck", "ping", "version",
"debug",
"-V", "--version", "-h", "--help":
return true
}
// If our parent is launchd, we're definitely not
// being run as a CLI.
if os.Getppid() == 1 {
return false
}
// Xcode adds the -NSDocumentRevisionsDebugMode flag on execution.
// If present, we are almost certainly being run as a GUI.
for _, arg := range os.Args {
if arg == "-NSDocumentRevisionsDebugMode" {
return false
}
}
// Looking at the environment of the GUI Tailscale app (ps eww
// $PID), empirically none of these environment variables are
// present. But all or some of these should be present with
// Terminal.all and bash or zsh.
for _, e := range []string{
"SHLVL",
"TERM",
"TERM_PROGRAM",
"PS1",
} {
if os.Getenv(e) != "" {
return true
}
}
return false
}
func newFlagSet(name string) *flag.FlagSet {
onError := flag.ExitOnError
if runtime.GOOS == "js" {
onError = flag.ContinueOnError
}
fs := flag.NewFlagSet(name, onError)
fs.SetOutput(Stderr)
return fs
}
// CleanUpArgs rewrites command line arguments for simplicity and backwards compatibility.
// In particular, it rewrites --authkey to --auth-key.
func CleanUpArgs(args []string) []string {
out := make([]string, 0, len(args))
for _, arg := range args {
// Rewrite --authkey to --auth-key, and --authkey=x to --auth-key=x,
// and the same for the -authkey variant.
switch {
case arg == "--authkey", arg == "-authkey":
arg = "--auth-key"
case strings.HasPrefix(arg, "--authkey="), strings.HasPrefix(arg, "-authkey="):
arg = strings.TrimLeft(arg, "-")
arg = strings.TrimPrefix(arg, "authkey=")
arg = "--auth-key=" + arg
}
out = append(out, arg)
}
return out
}
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) (err error) {
func Run(args []string) error {
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
args = []string{"version"}
}
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy &&
os.Getenv("GOKRAZY_FIRST_START") == "1" {
defer func() {
// Exit with 125 otherwise the CLI binary is restarted
// forever in a loop by the Gokrazy process supervisor.
// See https://gokrazy.org/userguide/process-interface/
if err != nil {
log.Println(err)
}
os.Exit(125)
}()
}
var warnOnce sync.Once
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
warnOnce.Do(func() {
fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
})
})
rootfs := newFlagSet("tailscale")
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
rootCmd := &ffcli.Command{
Name: "tailscale",
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
ShortUsage: "tailscale subcommand [flags]",
ShortHelp: "The easiest, most secure way to use WireGuard.",
LongHelp: strings.TrimSpace(`
For help on subcommands, add --help after: "tailscale status --help".
This CLI is still under active development. Commands and flags will
change in the future.
`),
Subcommands: []*ffcli.Command{
upCmd,
downCmd,
logoutCmd,
netcheckCmd,
ipCmd,
statusCmd,
pingCmd,
versionCmd,
webCmd,
fileCmd,
bugReportCmd,
certCmd,
adminCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
}
for _, c := range rootCmd.Subcommands {
c.UsageFunc = usageFunc
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
}
// Don't advertise the debug command, but it exists.
if strSliceContains(args, "debug") {
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
tailscale.TailscaledSocket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
tailscale.TailscaledSocketSetExplicitly = true
}
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
err := rootCmd.Run(context.Background())
if err == flag.ErrHelp {
return nil
}
return err
}
func fatalf(format string, a ...any) {
if Fatalf != nil {
Fatalf(format, a...)
return
}
func fatalf(format string, a ...interface{}) {
log.SetFlags(0)
log.Fatalf(format, a...)
}
// Fatalf, if non-nil, is used instead of log.Fatalf.
var Fatalf func(format string, a ...any)
var rootArgs struct {
socket string
}
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
c, err := safesocket.Connect(s)
c, err := safesocket.Connect(rootArgs.socket, 41112)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")
}
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
}
clientToServer := func(b []byte) {
ipn.WriteMsg(c, b)
@@ -253,14 +110,7 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
select {
case <-interrupt:
case <-ctx.Done():
// Context canceled elsewhere.
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
return
}
gotSignal.Set(true)
<-interrupt
c.Close()
cancel()
}()
@@ -270,22 +120,19 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
}
// pump receives backend messages on conn and pushes them into bc.
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
defer conn.Close()
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(conn)
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
return
}
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return fmt.Errorf("%w (tailscaled stopped running?)", err)
}
return err
log.Printf("ReadMsg: %v\n", err)
break
}
bc.GotNotifyMsg(msg)
}
return ctx.Err()
}
func strSliceContains(ss []string, s string) bool {
@@ -296,72 +143,3 @@ func strSliceContains(ss []string, s string) bool {
}
return false
}
func usageFunc(c *ffcli.Command) string {
var b strings.Builder
fmt.Fprintf(&b, "USAGE\n")
if c.ShortUsage != "" {
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
} else {
fmt.Fprintf(&b, " %s\n", c.Name)
}
fmt.Fprintf(&b, "\n")
if c.LongHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
}
if len(c.Subcommands) > 0 {
fmt.Fprintf(&b, "SUBCOMMANDS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
for _, subcommand := range c.Subcommands {
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
}
tw.Flush()
fmt.Fprintf(&b, "\n")
}
if countFlags(c.FlagSet) > 0 {
fmt.Fprintf(&b, "FLAGS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
c.FlagSet.VisitAll(func(f *flag.Flag) {
var s string
name, usage := flag.UnquoteUsage(f)
if isBoolFlag(f) {
s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
} else {
s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
if len(name) > 0 {
s += " " + name
}
}
// Four spaces before the tab triggers good alignment
// for both 4- and 8-space tab stops.
s += "\n \t"
s += strings.ReplaceAll(usage, "\n", "\n \t")
if f.DefValue != "" {
s += fmt.Sprintf(" (default %s)", f.DefValue)
}
fmt.Fprintln(&b, s)
})
tw.Flush()
fmt.Fprintf(&b, "\n")
}
return strings.TrimSpace(b.String())
}
func isBoolFlag(f *flag.Flag) bool {
bf, ok := f.Value.(interface {
IsBoolFlag() bool
})
return ok && bf.IsBoolFlag()
}
func countFlags(fs *flag.FlagSet) (n int) {
fs.VisitAll(func(*flag.Flag) { n++ })
return n
}

View File

@@ -1,923 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"reflect"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/version/distro"
)
// geese is a collection of gooses. It need not be complete.
// But it should include anything handled specially (e.g. linux, windows)
// and at least one thing that's not (darwin, freebsd).
var geese = []string{"linux", "darwin", "windows", "freebsd"}
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
// all flags. This will panic if a new flag creeps in that's unhandled.
//
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs.VisitAll(func(f *flag.Flag) {
mp := new(ipn.MaskedPrefs)
updateMaskedPrefsFromUpFlag(mp, f.Name)
got := mp.Pretty()
wantEmpty := preflessFlag(f.Name)
isEmpty := got == "MaskedPrefs{}"
if isEmpty != wantEmpty {
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
}
})
}
}
func TestCheckForAccidentalSettingReverts(t *testing.T) {
tests := []struct {
name string
flags []string // argv to be parsed by FlagSet
curPrefs *ipn.Prefs
curExitNodeIP netaddr.IP
curUser string // os.Getenv("USER") on the client side
goos string // empty means "linux"
distro distro.Distro
want string
}{
{
name: "bare_up_means_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
},
want: "",
},
{
name: "losing_hostname",
flags: []string{"--accept-dns"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
},
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
},
{
name: "hostname_changing_explicitly",
flags: []string{"--hostname=bar"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: "",
},
{
name: "hostname_changing_empty_explicitly",
flags: []string{"--hostname="},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: "",
},
{
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
// a fresh server's initial prefs.
name: "up_with_default_prefs",
flags: []string{"--authkey=foosdlkfjskdljf"},
curPrefs: ipn.NewPrefs(),
want: "",
},
{
name: "implicit_operator_change",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
OperatorUser: "alice",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
curUser: "eve",
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
},
{
name: "implicit_operator_matches_shell_user",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
OperatorUser: "alice",
},
curUser: "alice",
want: "",
},
{
name: "error_advertised_routes_exit_node_removed",
flags: []string{"--advertise-routes=10.0.42.0/24"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
},
{
name: "advertised_routes_exit_node_removed_explicit",
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: "",
},
{
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: "",
},
{
name: "advertise_exit_node", // Issue 1859
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "",
},
{
name: "advertise_exit_node_over_existing_routes",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "advertise_exit_node_over_existing_routes_and_exit_node",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "exit_node_clearing", // Issue 1777
flags: []string{"--exit-node="},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeID: "fooID",
},
want: "",
},
{
name: "remove_all_implicit",
flags: []string{"--force-reauth"},
curPrefs: &ipn.Prefs{
WantRunning: true,
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
},
curUser: "eve",
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
},
{
name: "remove_all_implicit_except_hostname",
flags: []string{"--hostname=newhostname"},
curPrefs: &ipn.Prefs{
WantRunning: true,
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
},
curUser: "eve",
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
},
{
name: "loggedout_is_implicit",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
LoggedOut: true,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "", // not an error. LoggedOut is implicit.
},
{
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
// values is able to enable exit nodes without warnings.
name: "make_windows_exit_node",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
RouteAll: true,
// And assume this no-op accidental pre-1.8 value:
NoSNAT: true,
},
goos: "windows",
want: "", // not an error
},
{
name: "ignore_netfilter_change_non_linux",
flags: []string{"--accept-dns"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
},
goos: "openbsd",
want: "", // not an error
},
{
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
flags: []string{"--operator=expbits"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
},
{
name: "errors_preserve_explicit_flags",
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: accidentalUpPrefix + " --auth-key=secretrand --force-reauth=false --reset --hostname=foo",
},
{
name: "error_exit_node_omit_with_ip_pref",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
},
{
name: "error_exit_node_omit_with_id_pref",
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeID: "some_stable_id",
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeAllowLANAccess: true,
ExitNodeID: "some_stable_id",
},
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
},
{
name: "ignore_login_server_synonym",
flags: []string{"--login-server=https://controlplane.tailscale.com"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "", // not an error
},
{
name: "ignore_login_server_synonym_on_other_change",
flags: []string{"--netfilter-mode=off"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: false,
NetfilterMode: preftype.NetfilterOn,
},
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: distro.Synology,
want: "",
},
{
// Same test case as "synology_permit_omit_accept_routes" above, but
// on non-Synology distro.
name: "not_synology_dont_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: "", // not Synology
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
goos := "linux"
if tt.goos != "" {
goos = tt.goos
}
var upArgs upArgsT
flagSet := newUpFlagSet(goos, &upArgs)
flags := CleanUpArgs(tt.flags)
flagSet.Parse(flags)
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
if err != nil {
t.Fatal(err)
}
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
var got string
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
distro: tt.distro,
}); err != nil {
got = err.Error()
}
if strings.TrimSpace(got) != tt.want {
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
}
})
}
}
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
fs := newUpFlagSet(goos, &args)
fs.Parse(flagArgs) // populates args
return
}
func TestPrefsFromUpArgs(t *testing.T) {
tests := []struct {
name string
args upArgsT
goos string // runtime.GOOS; empty means linux
st *ipnstate.Status // or nil
want *ipn.Prefs
wantErr string
wantWarn string
}{
{
name: "default_linux",
goos: "linux",
args: upArgsFromOSArgs("linux"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
NoSNAT: false,
NetfilterMode: preftype.NetfilterOn,
CorpDNS: true,
AllowSingleHosts: true,
},
},
{
name: "default_windows",
goos: "windows",
args: upArgsFromOSArgs("windows"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
},
{
name: "advertise_default_route",
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
AllowSingleHosts: true,
CorpDNS: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
NetfilterMode: preftype.NetfilterOn,
},
},
{
name: "error_advertise_route_invalid_ip",
args: upArgsT{
advertiseRoutes: "foo",
},
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
},
{
name: "error_advertise_route_unmasked_bits",
args: upArgsT{
advertiseRoutes: "1.2.3.4/16",
},
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
},
{
name: "error_exit_node_bad_ip",
args: upArgsT{
exitNodeIP: "foo",
},
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
},
{
name: "error_exit_node_allow_lan_without_exit_node",
args: upArgsT{
exitNodeAllowLANAccess: true,
},
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
},
{
name: "error_tag_prefix",
args: upArgsT{
advertiseTags: "foo",
},
wantErr: `tag: "foo": tags must start with 'tag:'`,
},
{
name: "error_long_hostname",
args: upArgsT{
hostname: strings.Repeat("a", 300),
},
wantErr: `hostname too long: 300 bytes (max 256)`,
},
{
name: "error_linux_netfilter_empty",
args: upArgsT{
netfilterMode: "",
},
wantErr: `invalid value --netfilter-mode=""`,
},
{
name: "error_linux_netfilter_bogus",
args: upArgsT{
netfilterMode: "bogus",
},
wantErr: `invalid value --netfilter-mode="bogus"`,
},
{
name: "error_exit_node_ip_is_self_ip",
args: upArgsT{
exitNodeIP: "100.105.106.107",
},
st: &ipnstate.Status{
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
},
wantErr: `cannot use 100.105.106.107 as an exit node as it is a local IP address to this machine; did you mean --advertise-exit-node?`,
},
{
name: "warn_linux_netfilter_nodivert",
goos: "linux",
args: upArgsT{
netfilterMode: "nodivert",
},
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
want: &ipn.Prefs{
WantRunning: true,
NetfilterMode: preftype.NetfilterNoDivert,
NoSNAT: true,
},
},
{
name: "warn_linux_netfilter_off",
goos: "linux",
args: upArgsT{
netfilterMode: "off",
},
wantWarn: "netfilter=off; configure iptables yourself.",
want: &ipn.Prefs{
WantRunning: true,
NetfilterMode: preftype.NetfilterOff,
NoSNAT: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var warnBuf tstest.MemLogger
goos := tt.goos
if goos == "" {
goos = "linux"
}
st := tt.st
if st == nil {
st = new(ipnstate.Status)
}
got, err := prefsFromUpArgs(tt.args, warnBuf.Logf, st, goos)
gotErr := fmt.Sprint(err)
if tt.wantErr != "" {
if tt.wantErr != gotErr {
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
}
return
}
if err != nil {
t.Fatal(err)
}
if tt.want == nil {
t.Fatal("tt.want is nil")
}
if !got.Equals(tt.want) {
jgot, _ := json.MarshalIndent(got, "", "\t")
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
if bytes.Equal(jgot, jwant) {
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
}
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
got.Pretty(), tt.want.Pretty(),
jgot, jwant,
)
}
})
}
}
func TestPrefFlagMapping(t *testing.T) {
prefHasFlag := map[string]bool{}
for _, pv := range prefsOfFlag {
for _, pref := range pv {
prefHasFlag[pref] = true
}
}
prefType := reflect.TypeOf(ipn.Prefs{})
for i := 0; i < prefType.NumField(); i++ {
prefName := prefType.Field(i).Name
if prefHasFlag[prefName] {
continue
}
switch prefName {
case "WantRunning", "Persist", "LoggedOut":
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
continue
case "OSVersion", "DeviceModel":
// Only used by Android, which doesn't have a CLI mode anyway, so
// fine to not map.
continue
case "NotepadURLs":
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
continue
}
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
}
}
func TestFlagAppliesToOS(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs.VisitAll(func(f *flag.Flag) {
if !flagAppliesToOS(f.Name, goos) {
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
}
})
}
}
func TestUpdatePrefs(t *testing.T) {
tests := []struct {
name string
flags []string // argv to be parsed into env.flagSet and env.upArgs
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
wantSimpleUp bool
wantJustEditMP *ipn.MaskedPrefs
wantErrSubtr string
}{
{
name: "bare_up_means_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
},
},
{
name: "just_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{
backendState: "Stopped",
},
wantSimpleUp: true,
},
{
name: "just_edit",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "just_edit_reset",
flags: []string{"--reset"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantJustEditMP: &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
AdvertiseTagsSet: true,
AllowSingleHostsSet: true,
ControlURLSet: true,
CorpDNSSet: true,
ExitNodeAllowLANAccessSet: true,
ExitNodeIDSet: true,
ExitNodeIPSet: true,
HostnameSet: true,
NetfilterModeSet: true,
NoSNATSet: true,
OperatorUserSet: true,
RouteAllSet: true,
RunSSHSet: true,
ShieldsUpSet: true,
WantRunningSet: true,
},
},
{
name: "control_synonym",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "change_login_server",
flags: []string{"--login-server=https://localhost:1000"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
wantErrSubtr: "can't change --login-server without --force-reauth",
},
{
name: "change_tags",
flags: []string{"--advertise-tags=tag:foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
t.Fatal(err)
}
simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
if err != nil {
if tt.wantErrSubtr != "" {
if !strings.Contains(err.Error(), tt.wantErrSubtr) {
t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
}
return
}
t.Fatal(err)
}
if simpleUp != tt.wantSimpleUp {
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
}
var oldEditPrefs ipn.Prefs
if justEditMP != nil {
oldEditPrefs = justEditMP.Prefs
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.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
}
})
}
}
var cmpIP = cmp.Comparer(func(a, b netaddr.IP) bool {
return a == b
})
func TestCleanUpArgs(t *testing.T) {
c := qt.New(t)
tests := []struct {
in []string
want []string
}{
{in: []string{"something"}, want: []string{"something"}},
{in: []string{}, want: []string{}},
{in: []string{"--authkey=0"}, want: []string{"--auth-key=0"}},
{in: []string{"a", "--authkey=1", "b"}, want: []string{"a", "--auth-key=1", "b"}},
{in: []string{"a", "--auth-key=2", "b"}, want: []string{"a", "--auth-key=2", "b"}},
{in: []string{"a", "-authkey=3", "b"}, want: []string{"a", "--auth-key=3", "b"}},
{in: []string{"a", "-auth-key=4", "b"}, want: []string{"a", "-auth-key=4", "b"}},
{in: []string{"a", "--authkey", "5", "b"}, want: []string{"a", "--auth-key", "5", "b"}},
{in: []string{"a", "-authkey", "6", "b"}, want: []string{"a", "--auth-key", "6", "b"}},
{in: []string{"a", "authkey", "7", "b"}, want: []string{"a", "authkey", "7", "b"}},
{in: []string{"--authkeyexpiry", "8"}, want: []string{"--authkeyexpiry", "8"}},
{in: []string{"--auth-key-expiry", "9"}, want: []string{"--auth-key-expiry", "9"}},
}
for _, tt := range tests {
got := CleanUpArgs(tt.in)
c.Assert(got, qt.DeepEquals, tt.want)
}
}

View File

@@ -1,85 +0,0 @@
// 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 cli
import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/hostinfo"
"tailscale.com/version/distro"
)
var configureHostCmd = &ffcli.Command{
Name: "configure-host",
Exec: runConfigureHost,
ShortHelp: "Configure Synology to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure-host' command is intended to run at boot as root
to create the /dev/net/tun device and give the tailscaled binary
permission to use it.
See: https://tailscale.com/kb/1152/synology-outbound/
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host")
return fs
})(),
}
var configureHostArgs struct{}
func runConfigureHost(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
return errors.New("only implemented on Synology")
}
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")
if !isDSM6 && !isDSM7 {
return fmt.Errorf("unsupported DSM version %q", osVer)
}
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if err := os.MkdirAll("/dev/net", 0755); err != nil {
return fmt.Errorf("creating /dev/net: %v", err)
}
if out, err := exec.Command("/bin/mknod", "/dev/net/tun", "c", "10", "200").CombinedOutput(); err != nil {
return fmt.Errorf("creating /dev/net/tun: %v, %s", err, out)
}
}
if err := os.Chmod("/dev/net/tun", 0666); err != nil {
return err
}
if isDSM6 {
fmt.Printf("/dev/net/tun exists and has permissions 0666. Skipping setcap on DSM6.\n")
return nil
}
const daemonBin = "/var/packages/Tailscale/target/bin/tailscaled"
if _, err := os.Stat(daemonBin); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("tailscaled binary not found at %s. Is the Tailscale *.spk package installed?", daemonBin)
}
return err
}
if out, err := exec.Command("/bin/setcap", "cap_net_admin,cap_net_raw+eip", daemonBin).CombinedOutput(); err != nil {
return fmt.Errorf("setcap: %v, %s", err, out)
}
fmt.Printf("Done. To restart Tailscale to use the new permissions, run:\n\n sudo synosystemctl restart pkgctl-Tailscale.service\n\n")
return nil
}

View File

@@ -1,350 +1,175 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/net/interfaces"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/monitor"
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
Name: "debug",
Exec: runDebug,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
return fs
})(),
Subcommands: []*ffcli.Command{
{
Name: "derp-map",
Exec: runDERPMap,
ShortHelp: "print DERP map",
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "print tailscaled's goroutines",
},
{
Name: "metrics",
Exec: runDaemonMetrics,
ShortHelp: "print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics")
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
return fs
})(),
},
{
Name: "env",
Exec: runEnv,
ShortHelp: "print cmd/tailscale environment",
},
{
Name: "hostinfo",
Exec: runHostinfo,
ShortHelp: "print hostinfo",
},
{
Name: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale local API",
},
{
Name: "restun",
Exec: localAPIAction("restun"),
ShortHelp: "force a magicsock restun",
},
{
Name: "rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "force a magicsock rebind",
},
{
Name: "prefs",
Exec: runPrefs,
ShortHelp: "print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
return fs
})(),
},
{
Name: "watch-ipn",
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
return fs
})(),
},
},
}
var debugArgs struct {
file string
cpuSec int
cpuFile string
memFile string
}
func writeProfile(dst string, v []byte) error {
if dst == "-" {
_, err := Stdout.Write(v)
return err
}
return os.WriteFile(dst, v, 0600)
}
func outName(dst string) string {
if dst == "-" {
return "stdout"
}
if runtime.GOOS == "darwin" {
return fmt.Sprintf("%s (warning: sandboxed macOS binaries write to Library/Containers; use - to write to stdout and redirect to file instead)", dst)
}
return dst
monitor bool
getURL string
derpCheck string
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
var usedFlag bool
if out := debugArgs.cpuFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
return err
}
log.Printf("CPU profile written to %s", outName(out))
}
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
if out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing memory profile ...")
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
return err
}
log.Printf("Memory profile written to %s", outName(out))
}
if debugArgs.monitor {
return runMonitor(ctx)
}
if debugArgs.file != "" {
usedFlag = true // TODO(bradfitz): add "file" subcommand
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
fatalf("%v\n", err)
}
e := json.NewEncoder(Stdout)
e.SetIndent("", "\t")
e.Encode(wfs)
return nil
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
}
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context) error {
dump := func() {
st, err := interfaces.GetState()
if err != nil {
return err
log.Printf("error getting state: %v", err)
return
}
log.Printf("Size: %v\n", size)
io.Copy(Stdout, rc)
return nil
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
if usedFlag {
// TODO(bradfitz): delete this path when all debug flags are migrated
// to subcommands.
return nil
}
return errors.New("see 'tailscale debug --help'")
}
func runLocalCreds(ctx context.Context, args []string) error {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
return nil
}
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
var prefsArgs struct {
pretty bool
}
func runPrefs(ctx context.Context, args []string) error {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if prefsArgs.pretty {
outln(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
outln(string(j))
}
return nil
}
var watchIPNArgs struct {
netmap bool
}
func runWatchIPN(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.SetNotifyCallback(func(n ipn.Notify) {
if !watchIPNArgs.netmap {
n.NetMap = nil
}
j, _ := json.MarshalIndent(n, "", "\t")
printf("%s\n", j)
mon, err := monitor.New(log.Printf, func() {
log.Printf("Link monitor fired. State:")
dump()
})
bc.RequestEngineStatus()
pump(ctx, bc, c)
return errors.New("exit")
}
func runDERPMap(ctx context.Context, args []string) error {
dm, err := tailscale.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
)
}
enc := json.NewEncoder(Stdout)
enc.SetIndent("", "\t")
enc.Encode(dm)
return nil
}
func localAPIAction(action string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected arguments")
}
return tailscale.DebugAction(ctx, action)
}
}
func runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)
}
return nil
}
func runHostinfo(ctx context.Context, args []string) error {
hi := hostinfo.New()
j, _ := json.MarshalIndent(hi, "", " ")
os.Stdout.Write(j)
return nil
}
func runDaemonGoroutines(ctx context.Context, args []string) error {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {
return err
}
Stdout.Write(goroutines)
return nil
log.Printf("Starting link change monitor; initial state:")
dump()
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
}
var metricsArgs struct {
watch bool
}
func runDaemonMetrics(ctx context.Context, args []string) error {
last := map[string]int64{}
for {
out, err := tailscale.DaemonMetrics(ctx)
if err != nil {
return err
}
if !metricsArgs.watch {
Stdout.Write(out)
return nil
}
bs := bufio.NewScanner(bytes.NewReader(out))
type change struct {
name string
from, to int64
}
var changes []change
var maxNameLen int
for bs.Scan() {
line := bytes.TrimSpace(bs.Bytes())
if len(line) == 0 || line[0] == '#' {
continue
}
f := strings.Fields(string(line))
if len(f) != 2 {
continue
}
name := f[0]
n, _ := strconv.ParseInt(f[1], 10, 64)
prev, ok := last[name]
if ok && prev == n {
continue
}
last[name] = n
if !ok {
continue
}
changes = append(changes, change{name, prev, n})
if len(name) > maxNameLen {
maxNameLen = len(name)
}
}
if len(changes) > 0 {
format := fmt.Sprintf("%%-%ds %%+5d => %%v\n", maxNameLen)
for _, c := range changes {
fmt.Fprintf(Stdout, format, c.name, c.to-c.from, c.to)
}
io.WriteString(Stdout, "\n")
}
time.Sleep(time.Second)
func getURL(ctx context.Context, urlStr string) error {
if urlStr == "login" {
urlStr = "https://login.tailscale.com"
}
log.SetOutput(os.Stdout)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
})
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
return fmt.Errorf("http.NewRequestWithContext: %v", err)
}
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
if err != nil {
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
}
log.Printf("proxy: %v", proxyURL)
tr := &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
ProxyConnectHeader: http.Header{},
DisableKeepAlives: true,
}
if proxyURL != nil {
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
if err == nil && auth != "" {
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
}
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
}
res, err := tr.RoundTrip(req)
if err != nil {
return fmt.Errorf("Transport.RoundTrip: %v", err)
}
defer res.Body.Close()
return res.Write(os.Stdout)
}
func checkDerp(ctx context.Context, derpRegion string) error {
dmap := derpmap.Prod()
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
return r
}
}
for _, r := range dmap.Regions {
log.Printf("Known region: %q", r.RegionCode)
}
log.Fatalf("unknown region %q", derpRegion)
panic("unreachable")
}
priv1 := key.NewPrivate()
priv2 := key.NewPrivate()
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
c2.NotePreferred(true) // just to open it
m, err := c2.Recv()
log.Printf("c2 got %T, %v", m, err)
t0 := time.Now()
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
return err
}
fmt.Println(time.Since(t0))
m, err = c2.Recv()
log.Printf("c2 got %T, %v", m, err)
if err != nil {
return err
}
log.Printf("ok")
return err
}

View File

@@ -1,56 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || windows || darwin
// +build linux windows darwin
package cli
import (
"fmt"
"path/filepath"
"runtime"
"strings"
ps "github.com/mitchellh/go-ps"
)
// fixTailscaledConnectError is called when the local tailscaled has
// been determined unreachable due to the provided origErr value. It
// returns either the same error or a better one to help the user
// understand why tailscaled isn't running for their platform.
func fixTailscaledConnectError(origErr error) error {
procs, err := ps.Processes()
if err != nil {
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
}
var foundProc ps.Process
for _, proc := range procs {
base := filepath.Base(proc.Executable())
if base == "tailscaled" {
foundProc = proc
break
}
if runtime.GOOS == "darwin" && base == "IPNExtension" {
foundProc = proc
break
}
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
foundProc = proc
break
}
}
if foundProc == nil {
switch runtime.GOOS {
case "windows":
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
case "darwin":
return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
case "linux":
return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running (sudo systemctl start tailscaled ?)")
}
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
}
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !linux && !windows && !darwin
// +build !linux,!windows,!darwin
package cli
import "fmt"
// The github.com/mitchellh/go-ps package doesn't work on all platforms,
// so just don't diagnose connect failures.
func fixTailscaledConnectError(origErr error) error {
return fmt.Errorf("failed to connect to local tailscaled process (is it running?); got: %w", origErr)
}

View File

@@ -6,10 +6,10 @@ package cli
import (
"context"
"fmt"
"log"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
)
@@ -23,22 +23,45 @@ var downCmd = &ffcli.Command{
func runDown(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
log.Fatalf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
if err != nil {
return fmt.Errorf("error fetching current status: %w", err)
}
if st.BackendState == "Stopped" {
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil
}
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,
},
WantRunningSet: true,
c, bc, ctx, cancel := connect(ctx)
defer cancel()
timer := time.AfterFunc(5*time.Second, func() {
log.Fatalf("timeout running stop")
})
return err
defer timer.Stop()
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
cur := n.Status.BackendState
switch cur {
case "Stopped":
log.Printf("already stopped")
cancel()
default:
log.Printf("was in state %q", cur)
}
return
}
if n.State != nil {
log.Printf("now in state %q", *n.State)
if *n.State == ipn.Stopped {
cancel()
}
return
}
log.Printf("Notify: %#v", n)
})
bc.RequestStatus()
bc.SetWantRunning(false)
pump(ctx, bc, c)
return nil
}

View File

@@ -1,512 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"unicode/utf8"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
var fileCmd = &ffcli.Command{
Name: "file",
ShortUsage: "file <cp|get> ...",
ShortHelp: "Send or receive files",
Subcommands: []*ffcli.Command{
fileCpCmd,
fileGetCmd,
},
Exec: func(context.Context, []string) error {
// TODO(bradfitz): is there a better ffcli way to
// annotate subcommand-required commands that don't
// have an exec body of their own?
return errors.New("file subcommand required; run 'tailscale file -h' for details")
},
}
var fileCpCmd = &ffcli.Command{
Name: "cp",
ShortUsage: "file cp <files...> <target>:",
ShortHelp: "Copy file(s) to a host",
Exec: runCp,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cp")
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
return fs
})(),
}
var cpArgs struct {
name string
verbose bool
targets bool
}
func runCp(ctx context.Context, args []string) error {
if cpArgs.targets {
return runCpTargets(ctx, args)
}
if len(args) < 2 {
return errors.New("usage: tailscale file cp <files...> <target>:")
}
files, target := args[:len(args)-1], args[len(args)-1]
if !strings.HasSuffix(target, ":") {
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
}
target = strings.TrimSuffix(target, ":")
hadBrackets := false
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
hadBrackets = true
target = strings.TrimSuffix(strings.TrimPrefix(target, "["), "]")
}
if ip, err := netaddr.ParseIP(target); err == nil && ip.Is6() && !hadBrackets {
return fmt.Errorf("an IPv6 literal must be written as [%s]", ip)
} else if hadBrackets && (err != nil || !ip.Is6()) {
return errors.New("unexpected brackets around target")
}
ip, _, err := tailscaleIPFromArg(ctx, target)
if err != nil {
return err
}
stableID, isOffline, err := getTargetStableID(ctx, ip)
if err != nil {
return fmt.Errorf("can't send to %s: %v", target, err)
}
if isOffline {
fmt.Fprintf(Stderr, "# warning: %s is offline\n", target)
}
if len(files) > 1 {
if cpArgs.name != "" {
return errors.New("can't use --name= with multiple files")
}
for _, fileArg := range files {
if fileArg == "-" {
return errors.New("can't use '-' as STDIN file when providing filename arguments")
}
}
}
for _, fileArg := range files {
var fileContents io.Reader
var name = cpArgs.name
var contentLength int64 = -1
if fileArg == "-" {
fileContents = os.Stdin
if name == "" {
name, fileContents, err = pickStdinFilename()
if err != nil {
return err
}
}
} else {
f, err := os.Open(fileArg)
if err != nil {
if version.IsSandboxedMacOS() {
return errors.New("the GUI version of Tailscale on macOS runs in a macOS sandbox that can't read files")
}
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
if fi.IsDir() {
return errors.New("directories not supported")
}
contentLength = fi.Size()
fileContents = io.LimitReader(f, contentLength)
if name == "" {
name = filepath.Base(fileArg)
}
if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
fileContents = &slowReader{r: fileContents}
}
}
if cpArgs.verbose {
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
if err != nil {
return err
}
if cpArgs.verbose {
log.Printf("sent %q", name)
}
}
return nil
}
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", false, err
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return "", false, err
}
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.IP() != ip {
continue
}
isOffline = n.Online != nil && !*n.Online
return n.StableID, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)
}
// fileTargetErrorDetail returns a non-nil error saying why ip is an
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
found := false
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
for _, pip := range peer.TailscaleIPs {
if pip == ip {
found = true
if peer.UserID != st.Self.UserID {
return errors.New("owned by different user; can only send files to your own devices")
}
}
}
}
}
if found {
return errors.New("target seems to be running an old Tailscale version")
}
if !tsaddr.IsTailscaleIP(ip) {
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
}
return errors.New("unknown target; not in your Tailnet")
}
const maxSniff = 4 << 20
func ext(b []byte) string {
if len(b) < maxSniff && utf8.Valid(b) {
return ".txt"
}
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
return exts[0]
}
return ""
}
// pickStdinFilename reads a bit of stdin to return a good filename
// for its contents. The returned Reader is the concatenation of the
// read and unread bits.
func pickStdinFilename() (name string, r io.Reader, err error) {
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
if err != nil {
return "", nil, err
}
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
}
type slowReader struct {
r io.Reader
rl *rate.Limiter
}
func (r *slowReader) Read(p []byte) (n int, err error) {
const burst = 4 << 10
plen := len(p)
if plen > burst {
plen = burst
}
if r.rl == nil {
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
}
n, err = r.r.Read(p[:plen])
r.rl.WaitN(context.Background(), n)
return
}
func runCpTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return err
}
for _, ft := range fts {
n := ft.Node
var detail string
if n.Online != nil {
if !*n.Online {
detail = "offline"
}
} else {
detail = "unknown-status"
}
if detail != "" && n.LastSeen != nil {
d := time.Since(*n.LastSeen)
detail += fmt.Sprintf("; last seen %v ago", d.Round(time.Minute))
}
if detail != "" {
detail = "\t" + detail
}
printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
}
return nil
}
// onConflict is a flag.Value for the --conflict flag's three string options.
type onConflict string
const (
skipOnExist onConflict = "skip"
overwriteExisting onConflict = "overwrite" // Overwrite any existing file at the target location
createNumberedFiles onConflict = "rename" // Create an alternately named file in the style of Chrome Downloads
)
func (v *onConflict) String() string { return string(*v) }
func (v *onConflict) Set(s string) error {
if s == "" {
*v = skipOnExist
return nil
}
*v = onConflict(strings.ToLower(s))
if *v != skipOnExist && *v != overwriteExisting && *v != createNumberedFiles {
return fmt.Errorf("%q is not one of (skip|overwrite|rename)", s)
}
return nil
}
var fileGetCmd = &ffcli.Command{
Name: "get",
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("get")
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory.
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
overwrite: overwrite existing file
rename: write to a new number-suffixed filename`)
return fs
})(),
}
var getArgs = struct {
wait bool
verbose bool
conflict onConflict
}{conflict: skipOnExist}
func numberedFileName(dir, name string, i int) string {
ext := path.Ext(name)
return filepath.Join(dir, fmt.Sprintf("%s (%d)%s",
strings.TrimSuffix(name, ext),
i, ext))
}
func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error) {
targetFile := filepath.Join(dir, base)
f, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
if err == nil {
return f, nil
}
// Something went wrong trying to open targetFile as a new file for writing.
switch action {
default:
// This should not happen.
return nil, fmt.Errorf("file issue. how to resolve this conflict? no one knows.")
case skipOnExist:
if _, statErr := os.Stat(targetFile); statErr == nil {
// we can stat a file at that path: so it already exists.
return nil, fmt.Errorf("refusing to overwrite file: %w", err)
}
return nil, fmt.Errorf("failed to write; %w", err)
case overwriteExisting:
// remove the target file and create it anew so we don't fall for an
// attacker who symlinks a known target name to a file he wants changed.
if err = os.Remove(targetFile); err != nil {
return nil, fmt.Errorf("unable to remove target file: %w", err)
}
if f, err = os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644); err != nil {
return nil, fmt.Errorf("unable to overwrite: %w", err)
}
return f, nil
case createNumberedFiles:
// It's possible the target directory or filesystem isn't writable by us,
// not just that the target file(s) already exists. For now, give up after
// a limited number of attempts. In future, maybe distinguish this case
// and follow in the style of https://tinyurl.com/chromium100
maxAttempts := 100
for i := 1; i < maxAttempts; i++ {
if f, err = os.OpenFile(numberedFileName(dir, base, i), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644); err == nil {
return f, nil
}
}
return nil, fmt.Errorf("unable to find a name for writing %v, final attempt: %w", targetFile, err)
}
}
func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targetFile string, size int64, err error) {
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
if err != nil {
return "", 0, fmt.Errorf("opening inbox file %q: %w", wf.Name, err)
}
f, err := openFileOrSubstitute(dir, wf.Name, getArgs.conflict)
if err != nil {
return "", 0, err
}
_, err = io.Copy(f, rc)
rc.Close()
if err != nil {
return "", 0, fmt.Errorf("failed to write %v: %v", f.Name(), err)
}
return f.Name(), size, f.Close()
}
func runFileGet(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: file get <target-directory>")
}
log.SetFlags(0)
dir := args[0]
if dir == "/dev/null" {
return wipeInbox(ctx)
}
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
return fmt.Errorf("%q is not a directory", dir)
}
var wfs []apitype.WaitingFile
var err error
for {
wfs, err = tailscale.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %w", err)
}
if len(wfs) != 0 || !getArgs.wait {
break
}
if getArgs.verbose {
printf("waiting for file...")
}
if err := waitForFile(ctx); err != nil {
return err
}
}
var errs []error
deleted := 0
for _, wf := range wfs {
writtenFile, size, err := receiveFile(ctx, wf, dir)
if err != nil {
errs = append(errs, err)
continue
}
if getArgs.verbose {
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
}
if err = tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
errs = append(errs, fmt.Errorf("deleting %q from inbox: %v", wf.Name, err))
continue
}
deleted++
}
if getArgs.verbose {
printf("moved %d/%d files\n", deleted, len(wfs))
}
if len(errs) == 0 {
return nil
}
for _, err := range errs[:len(errs)-1] {
outln(err)
}
return errs[len(errs)-1]
}
func wipeInbox(ctx context.Context) error {
if getArgs.wait {
return errors.New("can't use --wait with /dev/null target")
}
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %w", err)
}
deleted := 0
for _, wf := range wfs {
if getArgs.verbose {
log.Printf("deleting %v ...", wf.Name)
}
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
return fmt.Errorf("deleting %q: %v", wf.Name, err)
}
deleted++
}
if getArgs.verbose {
log.Printf("deleted %d files", deleted)
}
return nil
}
func waitForFile(ctx context.Context) error {
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
fileWaiting := make(chan bool, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
fatalf("Notify.ErrMessage: %v\n", *n.ErrMessage)
}
if n.FilesWaiting != nil {
select {
case fileWaiting <- true:
default:
}
}
})
go pump(pumpCtx, bc, c)
select {
case <-fileWaiting:
return nil
case <-pumpCtx.Done():
return pumpCtx.Err()
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -1,123 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn/ipnstate"
)
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ip")
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
return fs
})(),
}
var ipArgs struct {
want1 bool
want4 bool
want6 bool
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 1 {
return errors.New("too many arguments, expected at most one peer")
}
var of string
if len(args) == 1 {
of = args[0]
}
v4, v6 := ipArgs.want4, ipArgs.want6
nflags := 0
for _, b := range []bool{ipArgs.want1, v4, v6} {
if b {
nflags++
}
}
if nflags > 1 {
return errors.New("tailscale ip -1, -4, and -6 are mutually exclusive")
}
if !v4 && !v6 {
v4, v6 = true, true
}
st, err := tailscale.Status(ctx)
if err != nil {
return err
}
ips := st.TailscaleIPs
if of != "" {
ip, _, err := tailscaleIPFromArg(ctx, of)
if err != nil {
return err
}
peer, ok := peerMatchingIP(st, ip)
if !ok {
return fmt.Errorf("no peer found with IP %v", ip)
}
ips = peer.TailscaleIPs
}
if len(ips) == 0 {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
if ipArgs.want1 {
ips = ips[:1]
}
match := false
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {
match = true
outln(ip)
}
}
if !match {
if ipArgs.want4 {
return errors.New("no Tailscale IPv4 address")
}
if ipArgs.want6 {
return errors.New("no Tailscale IPv6 address")
}
}
return nil
}
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return
}
for _, ps = range st.Peer {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
if ps := st.Self; ps != nil {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
return nil, false
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"fmt"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var logoutCmd = &ffcli.Command{
Name: "logout",
ShortUsage: "logout [flags]",
ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(`
"tailscale logout" brings the network down and invalidates
the current node key, forcing a future use of it to cause
a reauthentication.
`),
Exec: runLogout,
}
func runLogout(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
return tailscale.Logout(ctx)
}

View File

@@ -9,20 +9,16 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/net/dnscache"
"tailscale.com/net/netcheck"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
)
@@ -33,7 +29,7 @@ var netcheckCmd = &ffcli.Command{
ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netcheck")
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
@@ -49,8 +45,7 @@ var netcheckArgs struct {
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
DNSCache: dnscache.Get(),
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
@@ -60,20 +55,10 @@ func runNetcheck(ctx context.Context, args []string) error {
}
if strings.HasPrefix(netcheckArgs.format, "json") {
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm, err := tailscale.CurrentDERPMap(ctx)
noRegions := dm != nil && len(dm.Regions) == 0
if noRegions {
log.Printf("No DERP map from tailscaled; using default.")
}
if err != nil || noRegions {
dm, err = prodDERPMap(ctx, http.DefaultClient)
if err != nil {
return err
}
}
dm := derpmap.Prod()
for {
t0 := time.Now()
report, err := c.GetReport(ctx, dm)
@@ -82,7 +67,7 @@ func runNetcheck(ctx context.Context, args []string) error {
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
}
if err != nil {
return fmt.Errorf("netcheck: %w", err)
log.Fatalf("netcheck: %v", err)
}
if err := printReport(dm, report); err != nil {
return err
@@ -112,36 +97,36 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
}
if j != nil {
j = append(j, '\n')
Stdout.Write(j)
os.Stdout.Write(j)
return nil
}
printf("\nReport:\n")
printf("\t* UDP: %v\n", report.UDP)
fmt.Printf("\nReport:\n")
fmt.Printf("\t* UDP: %v\n", report.UDP)
if report.GlobalV4 != "" {
printf("\t* IPv4: yes, %v\n", report.GlobalV4)
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
} else {
printf("\t* IPv4: (no addr found)\n")
fmt.Printf("\t* IPv4: (no addr found)\n")
}
if report.GlobalV6 != "" {
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
} else if report.IPv6 {
printf("\t* IPv6: (no addr found)\n")
fmt.Printf("\t* IPv6: (no addr found)\n")
} else {
printf("\t* IPv6: no\n")
fmt.Printf("\t* IPv6: no\n")
}
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
printf("\t* HairPinning: %v\n", report.HairPinning)
printf("\t* PortMapping: %v\n", portMapping(report))
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
fmt.Printf("\t* PortMapping: %v\n", portMapping(report))
// When DERP latency checking failed,
// magicsock will try to pick the DERP server that
// most of your other nodes are also using
if len(report.RegionLatency) == 0 {
printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
} else {
printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
printf("\t* DERP latency:\n")
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
fmt.Printf("\t* DERP latency:\n")
var rids []int
for rid := range dm.Regions {
rids = append(rids, rid)
@@ -168,7 +153,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
if netcheckArgs.verbose {
derpNum = fmt.Sprintf("derp%d, ", rid)
}
printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
}
}
return nil
@@ -190,27 +175,3 @@ func portMapping(r *netcheck.Report) string {
}
return strings.Join(got, ", ")
}
func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return nil, fmt.Errorf("create prodDERPMap request: %w", err)
}
res, err := httpc.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b)
}
var derpMap tailcfg.DERPMap
if err = json.Unmarshal(b, &derpMap); err != nil {
return nil, fmt.Errorf("fetch prodDERPMap: %w", err)
}
return &derpMap, nil
}

View File

@@ -11,12 +11,10 @@ import (
"fmt"
"log"
"net"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
)
@@ -46,10 +44,9 @@ relay node.
`),
Exec: runPing,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ping")
fs := flag.NewFlagSet("ping", flag.ExitOnError)
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
return fs
@@ -60,72 +57,53 @@ var pingArgs struct {
num int
untilDirect bool
verbose bool
tsmp bool
timeout time.Duration
}
func runPing(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
c, bc, ctx, cancel := connect(ctx)
defer cancel()
if len(args) != 1 || args[0] == "" {
if len(args) != 1 {
return errors.New("usage: ping <hostname-or-IP>")
}
var ip string
prc := make(chan *ipnstate.PingResult, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
fatalf("Notify.ErrMessage: %v", *n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
prc <- pr
}
})
pumpErr := make(chan error, 1)
go func() { pumpErr <- pump(ctx, bc, c) }()
hostOrIP := args[0]
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
if err != nil {
return err
var ip string
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
if self {
printf("%v is local Tailscale IP\n", ip)
return nil
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
ch := make(chan *ipnstate.PingResult, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
ch <- pr
}
})
go pump(ctx, bc, c)
n := 0
anyPong := false
for {
n++
bc.Ping(ip, pingArgs.tsmp)
bc.Ping(ip)
timer := time.NewTimer(pingArgs.timeout)
select {
case <-timer.C:
printf("timeout waiting for ping reply\n")
case err := <-pumpErr:
return err
case pr := <-prc:
fmt.Printf("timeout waiting for ping reply\n")
case pr := <-ch:
timer.Stop()
if pr.Err != "" {
if pr.IsLocalIP {
outln(pr.Err)
return nil
}
return errors.New(pr.Err)
}
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
@@ -133,20 +111,8 @@ func runPing(ctx context.Context, args []string) error {
if pr.DERPRegionID != 0 {
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
}
if pingArgs.tsmp {
// TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
// For now just say it came via TSMP.
via = "TSMP"
}
anyPong = true
extra := ""
if pr.PeerAPIPort != 0 {
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
}
printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
if pingArgs.tsmp {
return nil
}
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
if pr.Endpoint != "" && pingArgs.untilDirect {
return nil
}
@@ -158,47 +124,7 @@ func runPing(ctx context.Context, args []string) error {
if !anyPong {
return errors.New("no reply")
}
if pingArgs.untilDirect {
return errors.New("direct connection not established")
}
return nil
}
}
}
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, self bool, err error) {
// If the argument is an IP address, use it directly without any resolution.
if net.ParseIP(hostOrIP) != nil {
return hostOrIP, false, nil
}
// Otherwise, try to resolve it first from the network peer list.
st, err := tailscale.Status(ctx)
if err != nil {
return "", false, err
}
match := func(ps *ipnstate.PeerStatus) bool {
return strings.EqualFold(hostOrIP, dnsOrQuoteHostname(st, ps)) || hostOrIP == ps.DNSName
}
for _, ps := range st.Peer {
if match(ps) {
if len(ps.TailscaleIPs) == 0 {
return "", false, errors.New("node found but lacks an IP")
}
return ps.TailscaleIPs[0].String(), false, nil
}
}
if match(st.Self) && len(st.Self.TailscaleIPs) > 0 {
return st.Self.TailscaleIPs[0].String(), true, nil
}
// Finally, use DNS.
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return "", false, fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return "", false, fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
return addrs[0], false, nil
}
}

View File

@@ -10,49 +10,31 @@ import (
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
"tailscale.com/util/dnsname"
)
var statusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status [--active] [--web] [--json]",
ShortUsage: "status [-active] [-web] [-json]",
ShortHelp: "Show state of tailscaled and its connections",
LongHelp: strings.TrimSpace(`
JSON FORMAT
Warning: this format has changed between releases and might change more
in the future.
For a description of the fields, see the "type Status" declaration at:
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
(and be sure to select branch/tag that corresponds to the version
of Tailscale you're running)
`),
Exec: runStatus,
Exec: runStatus,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("status")
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
return fs
})(),
@@ -65,18 +47,42 @@ var statusArgs struct {
browser bool // in web mode, whether to open browser
active bool // in CLI mode, filter output to only peers with active sessions
self bool // in CLI mode, show status of local machine
peers bool // in CLI mode, show status of peer machines
}
func runStatus(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.AllowVersionSkew = true
ch := make(chan *ipnstate.Status, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
ch <- n.Status
}
})
go pump(ctx, bc, c)
getStatus := func() (*ipnstate.Status, error) {
bc.RequestStatus()
select {
case st := <-ch:
return st, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
st, err := getStatus()
if err != nil {
return fixTailscaledConnectError(err)
return err
}
if statusArgs.json {
if statusArgs.active {
for peer, ps := range st.Peer {
if !ps.Active {
if !peerActive(ps) {
delete(st.Peer, peer)
}
}
@@ -85,7 +91,7 @@ func runStatus(ctx context.Context, args []string) error {
if err != nil {
return err
}
printf("%s", j)
fmt.Printf("%s", j)
return nil
}
if statusArgs.web {
@@ -94,7 +100,7 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
statusURL := interfaces.HTTPOfListener(ln)
printf("Serving Tailscale status at %v ...\n", statusURL)
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL)
go func() {
<-ctx.Done()
ln.Close()
@@ -107,7 +113,7 @@ func runStatus(ctx context.Context, args []string) error {
http.NotFound(w, r)
return
}
st, err := tailscale.Status(ctx)
st, err := getStatus()
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -121,65 +127,39 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
if st.BackendState == ipn.Stopped.String() {
fmt.Println("Tailscale is stopped.")
os.Exit(1)
}
if len(st.Health) > 0 {
printf("# Health check:\n")
for _, m := range st.Health {
printf("# - %s\n", m)
}
outln()
}
var buf bytes.Buffer
f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) {
f("%-15s %-20s %-12s %-7s ",
firstIPString(ps.TailscaleIPs),
dnsOrQuoteHostname(st, ps),
ownerLogin(st, ps),
active := peerActive(ps)
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
ps.PublicKey.ShortString(),
ps.OS,
ps.TailAddr,
ps.SimpleHostName(),
ps.TxBytes,
ps.RxBytes,
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
var offline string
if !ps.Online {
offline = "; offline"
}
if !ps.Active {
if ps.ExitNode {
f("idle; exit node" + offline)
} else if ps.ExitNodeOption {
f("idle; offers exit node" + offline)
} else if anyTraffic {
f("idle" + offline)
} else if !ps.Online {
f("offline")
} else {
f("-")
}
if active && relay != "" && ps.CurAddr == "" {
relay = "*" + relay + "*"
} else {
f("active; ")
if ps.ExitNode {
f("exit node; ")
} else if ps.ExitNodeOption {
f("offers exit node; ")
}
if relay != "" && ps.CurAddr == "" {
f("relay %q", relay)
} else if ps.CurAddr != "" {
f("direct %s", ps.CurAddr)
}
if !ps.Online {
f("; offline")
}
relay = " " + relay
}
if anyTraffic {
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
f("%-6s", relay)
for i, addr := range ps.Addrs {
if i != 0 {
f(", ")
}
if addr == ps.CurAddr {
f("*%s*", addr)
} else {
f("%s", addr)
}
}
f("\n")
}
@@ -187,73 +167,21 @@ func runStatus(ctx context.Context, args []string) error {
if statusArgs.self && st.Self != nil {
printPS(st.Self)
}
if statusArgs.peers {
var peers []*ipnstate.PeerStatus
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
peers = append(peers, ps)
}
ipnstate.SortPeers(peers)
for _, ps := range peers {
if statusArgs.active && !ps.Active {
continue
}
printPS(ps)
for _, peer := range st.Peers() {
ps := st.Peer[peer]
active := peerActive(ps)
if statusArgs.active && !active {
continue
}
printPS(ps)
}
Stdout.Write(buf.Bytes())
os.Stdout.Write(buf.Bytes())
return nil
}
// isRunningOrStarting reports whether st is in state Running or Starting.
// It also returns a description of the status suitable to display to a user.
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
switch st.BackendState {
default:
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
case ipn.Stopped.String():
return "Tailscale is stopped.", false
case ipn.NeedsLogin.String():
s := "Logged out."
if st.AuthURL != "" {
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
}
return s, false
case ipn.NeedsMachineAuth.String():
return "Machine is not yet authorized by tailnet admin.", false
case ipn.Running.String(), ipn.Starting.String():
return st.BackendState, true
}
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {
return baseName
}
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
}
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
if ps.UserID.IsZero() {
return "-"
}
u, ok := st.User[ps.UserID]
if !ok {
return fmt.Sprint(ps.UserID)
}
if i := strings.Index(u.LoginName, "@"); i != -1 {
return u.LoginName[:i+1]
}
return u.LoginName
}
func firstIPString(v []netaddr.IP) string {
if len(v) == 0 {
return ""
}
return v[0].String()
// peerActive reports whether ps has recent activity.
//
// TODO: have the server report this bool instead.
func peerActive(ps *ipnstate.PeerStatus) bool {
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,10 @@ import (
"context"
"flag"
"fmt"
"log"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
"tailscale.com/version"
)
@@ -19,7 +20,7 @@ var versionCmd = &ffcli.Command{
ShortUsage: "version [flags]",
ShortHelp: "Print Tailscale version",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("version")
fs := flag.NewFlagSet("version", flag.ExitOnError)
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
return fs
})(),
@@ -32,19 +33,37 @@ var versionArgs struct {
func runVersion(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
log.Fatalf("too many non-flag arguments: %q", args)
}
if !versionArgs.daemon {
outln(version.String())
fmt.Println(version.LONG)
return nil
}
fmt.Printf("Client: %s\n", version.LONG)
printf("Client: %s\n", version.String())
c, bc, ctx, cancel := connect(ctx)
defer cancel()
st, err := tailscale.StatusWithoutPeers(ctx)
if err != nil {
return err
bc.AllowVersionSkew = true
done := make(chan struct{})
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
fmt.Printf("Daemon: %s\n", n.Version)
close(done)
}
})
go pump(ctx, bc, c)
bc.RequestStatus()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
printf("Daemon: %s\n", st.Version)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,491 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"crypto/tls"
_ "embed"
"encoding/json"
"encoding/xml"
"flag"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/cgi"
"net/url"
"os"
"os/exec"
"runtime"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
"tailscale.com/util/groupmember"
"tailscale.com/version/distro"
)
//go:embed web.html
var webHTML string
//go:embed web.css
var webCSS string
//go:embed auth-redirect.html
var authenticationRedirectHTML string
var tmpl *template.Template
func init() {
tmpl = template.Must(template.New("web.html").Parse(webHTML))
template.Must(tmpl.New("web.css").Parse(webCSS))
}
type tmplData struct {
Profile tailcfg.UserProfile
SynologyUser string
Status string
DeviceName string
IP string
AdvertiseExitNode bool
AdvertiseRoutes string
}
var webCmd = &ffcli.Command{
Name: "web",
ShortUsage: "web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(`
"tailscale web" runs a webserver for controlling the Tailscale daemon.
It's primarily intended for use on Synology, QNAP, and other
NAS devices where a web interface is the natural place to control
Tailscale, as opposed to a CLI or a native app.
`),
FlagSet: (func() *flag.FlagSet {
webf := newFlagSet("web")
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
return webf
})(),
Exec: runWeb,
}
var webArgs struct {
listen string
cgi bool
}
func tlsConfigFromEnvironment() *tls.Config {
crt := os.Getenv("TLS_CRT_PEM")
key := os.Getenv("TLS_KEY_PEM")
if crt == "" || key == "" {
return nil
}
// We support passing in the complete certificate and key from environment
// variables because pfSense stores its cert+key in the PHP config. We populate
// TLS_CRT_PEM and TLS_KEY_PEM from PHP code before starting tailscale web.
// These are the PEM-encoded Certificate and Private Key.
cert, err := tls.X509KeyPair([]byte(crt), []byte(key))
if err != nil {
log.Printf("tlsConfigFromEnvironment: %v", err)
// Fallback to unencrypted HTTP.
return nil
}
return &tls.Config{Certificates: []tls.Certificate{cert}}
}
func runWeb(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
if webArgs.cgi {
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
log.Printf("tailscale.cgi: %v", err)
return err
}
return nil
}
tlsConfig := tlsConfigFromEnvironment()
if tlsConfig != nil {
server := &http.Server{
Addr: webArgs.listen,
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(webHandler),
}
log.Printf("web server runNIng on: https://%s", server.Addr)
return server.ListenAndServeTLS("", "")
} else {
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
}
// urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr)
if host == "" {
host = "127.0.0.1"
}
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
}
// authorize returns the name of the user accessing the web UI after verifying
// whether the user has access to the web UI. The function will write the
// error to the provided http.ResponseWriter.
// Note: This is different from a tailscale user, and is typically the local
// user on the node.
func authorize(w http.ResponseWriter, r *http.Request) (string, error) {
switch distro.Get() {
case distro.Synology:
user, err := synoAuthn()
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return "", err
}
if err := authorizeSynology(user); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return "", err
}
return user, nil
case distro.QNAP:
user, resp, err := qnapAuthn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return "", err
}
if resp.IsAdmin == 0 {
http.Error(w, err.Error(), http.StatusForbidden)
return "", err
}
return user, nil
}
return "", nil
}
// authorizeSynology checks whether the provided user has access to the web UI
// by consulting the membership of the "administrators" group.
func authorizeSynology(name string) error {
yes, err := groupmember.IsMemberOfGroup("administrators", name)
if err != nil {
return err
}
if !yes {
return fmt.Errorf("not a member of administrators group")
}
return nil
}
type qnapAuthResponse struct {
AuthPassed int `xml:"authPassed"`
IsAdmin int `xml:"isAdmin"`
AuthSID string `xml:"authSid"`
ErrorValue int `xml:"errorValue"`
}
func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
user, err := r.Cookie("NAS_USER")
if err != nil {
return "", nil, err
}
token, err := r.Cookie("qtoken")
if err != nil {
return "", nil, err
}
query := url.Values{
"qtoken": []string{token.Value},
"user": []string{user.Value},
}
u := url.URL{
Scheme: r.URL.Scheme,
Host: r.URL.Host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
resp, err := http.Get(u.String())
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
authResp := &qnapAuthResponse{}
if err := xml.Unmarshal(out, authResp); err != nil {
return "", nil, err
}
if authResp.AuthPassed == 0 {
return "", nil, fmt.Errorf("not authenticated")
}
return user.Value, authResp, nil
}
func synoAuthn() (string, error) {
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("auth: %v: %s", err, out)
}
return strings.TrimSpace(string(out)), nil
}
func authRedirect(w http.ResponseWriter, r *http.Request) bool {
if distro.Get() == distro.Synology {
return synoTokenRedirect(w, r)
}
return false
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("X-Syno-Token") != "" {
return false
}
if r.URL.Query().Get("SynoToken") != "" {
return false
}
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
return false
}
// We need a SynoToken for authenticate.cgi.
// So we tell the client to get one.
serverURL := r.URL.Scheme + "://" + r.URL.Host
synoTokenRedirectHTML.Execute(w, serverURL)
return true
}
var synoTokenRedirectHTML = template.Must(template.New("redirect").Parse(`<html><body>
Redirecting with session token...
<script>
var serverURL = {{ . }};
var req = new XMLHttpRequest();
req.overrideMimeType("application/json");
req.open("GET", serverURL + "/webman/login.cgi", true);
req.onload = function() {
var jsonResponse = JSON.parse(req.responseText);
var token = jsonResponse["SynoToken"];
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token;
};
req.send(null);
</script>
</body></html>
`))
func webHandler(w http.ResponseWriter, r *http.Request) {
if authRedirect(w, r) {
return
}
user, err := authorize(w, r)
if err != nil {
return
}
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
w.Write([]byte(authenticationRedirectHTML))
return
}
if r.Method == "POST" {
defer r.Body.Close()
var postData struct {
AdvertiseRoutes string
AdvertiseExitNode bool
Reauthenticate bool
}
type mi map[string]any
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
prefs, err := tailscale.GetPrefs(r.Context())
if err != nil && !postData.Reauthenticate {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
} else {
routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
prefs.AdvertiseRoutes = routes
}
w.Header().Set("Content-Type", "application/json")
url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
if url != "" {
json.NewEncoder(w).Encode(mi{"url": url})
} else {
io.WriteString(w, "{}")
}
return
}
st, err := tailscale.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
prefs, err := tailscale.GetPrefs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0]
data := tmplData{
SynologyUser: user,
Profile: profile,
Status: st.BackendState,
DeviceName: deviceName,
}
exitNodeRouteV4 := netaddr.MustParseIPPrefix("0.0.0.0/0")
exitNodeRouteV6 := netaddr.MustParseIPPrefix("::/0")
for _, r := range prefs.AdvertiseRoutes {
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
data.AdvertiseExitNode = true
} else {
if data.AdvertiseRoutes != "" {
data.AdvertiseRoutes += ","
}
data.AdvertiseRoutes += r.String()
}
}
if len(st.TailscaleIPs) != 0 {
data.IP = st.TailscaleIPs[0].String()
}
buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(buf.Bytes())
}
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authURL string, retErr error) {
if prefs == nil {
prefs = ipn.NewPrefs()
prefs.ControlURL = ipn.DefaultControlURL
prefs.WantRunning = true
prefs.CorpDNS = true
prefs.AllowSingleHosts = true
prefs.ForceDaemon = (runtime.GOOS == "windows")
}
if distro.Get() == distro.Synology {
prefs.NetfilterMode = preftype.NetfilterOff
}
st, err := tailscale.Status(ctx)
if err != nil {
return "", fmt.Errorf("can't fetch status: %v", err)
}
origAuthURL := st.AuthURL
// printAuthURL reports whether we should print out the
// provided auth URL from an IPN notify.
printAuthURL := func(url string) bool {
return url != origAuthURL
}
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
go pump(pumpCtx, bc, c)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.Engine != nil {
select {
case gotEngineUpdate <- true:
default:
}
}
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
authURL = *url
cancel()
}
if !forceReauth && n.Prefs != nil {
p1, p2 := *n.Prefs, *prefs
p1.Persist = nil
p2.Persist = nil
if p1.Equals(&p2) {
cancel()
}
}
})
// Wait for backend client to be connected so we know
// we're subscribed to updates. Otherwise we can miss
// an update upon its transition to running. Do so by causing some traffic
// back to the bus that we then wait on.
bc.RequestEngineStatus()
select {
case <-gotEngineUpdate:
case <-pumpCtx.Done():
return authURL, pumpCtx.Err()
}
bc.SetPrefs(prefs)
bc.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
})
if forceReauth {
bc.StartLoginInteractive()
}
<-pumpCtx.Done() // wait for authURL or complete failure
if authURL == "" && retErr == nil {
if !forceReauth {
return "", nil // no auth URL is fine
}
retErr = pumpCtx.Err()
}
if authURL == "" && retErr == nil {
return "", fmt.Errorf("login failed with no backend error message")
}
return authURL, retErr
}

View File

@@ -1,170 +0,0 @@
<!doctype html>
<html class="bg-gray-50">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<title>Tailscale</title>
<style>{{template "web.css"}}</style>
</head>
<body class="py-14">
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-4">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile.LoginName }}
<div class="text-right truncate leading-4">
<h4 class="truncate leading-normal">{{.}}</h4>
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
</div>
{{ end }}
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{{ with .Profile.ProfilePicURL }}
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style="background-image: url('{{.}}'); background-size: cover;"></div>
{{ else }}
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
{{ end }}
</div>
</div>
</header>
{{ if .IP }}
<div
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
<div class="flex items-center min-width-0">
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
</div>
<h5>{{.IP}}</h5>
</div>
{{ end }}
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
{{ if .IP }}
<div class="mb-6">
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Reauthenticate</button>
</a>
{{ else }}
<div class="mb-6">
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or,&nbsp;learn&nbsp;more at <a
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Log In</button>
</a>
{{ end }}
{{ else if eq .Status "NeedsMachineAuth" }}
<div class="mb-4">
This device is authorized, but needs approval from a network admin before it can connect to the network.
</div>
{{ else }}
<div class="mb-4">
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
</div>
<div class="mb-4">
<a href="#" class="mb-4 js-advertiseExitNode">
{{if .AdvertiseExitNode}}
<button class="button button-red button-medium" id="enabled">Stop advertising Exit Node</button>
{{else}}
<button class="button button-blue button-medium" id="enabled">Advertise as Exit Node</button>
{{end}}
</a>
</div>
<div class="mb-4">
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
</div>
{{ end }}
</main>
<script>(function () {
const advertiseExitNode = {{.AdvertiseExitNode}};
let fetchingUrl = false;
var data = {
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
AdvertiseExitNode: advertiseExitNode,
Reauthenticate: false
};
function postData(e) {
e.preventDefault();
if (fetchingUrl) {
return;
}
fetchingUrl = true;
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("SynoToken");
const nextParams = new URLSearchParams({ up: true });
if (token) {
nextParams.set("SynoToken", token)
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
const url = nextUrl.toString();
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(data)
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
if (err) {
throw new Error(err);
}
const url = res["url"];
if (url) {
document.location.href = url;
} else {
location.reload();
}
}).catch(err => {
alert("Failed to log in: " + err.message);
});
}
Array.from(document.querySelectorAll(".js-loginButton")).forEach(el => {
el.addEventListener("click", function(e) {
data.Reauthenticate = true;
postData(e);
});
})
Array.from(document.querySelectorAll(".js-advertiseExitNode")).forEach(el => {
el.addEventListener("click", function(e) {
data.AdvertiseExitNode = !advertiseExitNode;
postData(e);
});
})
})();</script>
</body>
</html>

View File

@@ -1,44 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import "testing"
func TestUrlOfListenAddr(t *testing.T) {
tests := []struct {
name string
in, want string
}{
{
name: "TestLocalhost",
in: "localhost:8088",
want: "http://localhost:8088",
},
{
name: "TestNoHost",
in: ":8088",
want: "http://127.0.0.1:8088",
},
{
name: "TestExplicitHost",
in: "127.0.0.2:8088",
want: "http://127.0.0.2:8088",
},
{
name: "TestIPv6",
in: "[::1]:8088",
want: "http://[::1]:8088",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := urlOfListenAddr(tt.in)
if url != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, url)
}
})
}
}

View File

@@ -1,131 +1,141 @@
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/derp+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
💣 go4.org/mem from tailscale.com/control/controlclient+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
tailscale.com from tailscale.com/version
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
tailscale.com/disco from tailscale.com/derp
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
tailscale.com/hostinfo from tailscale.com/net/interfaces+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
💣 tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/views from tailscale.com/tailcfg+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/lineread from tailscale.com/net/interfaces+
W tailscale.com/util/winutil from tailscale.com/hostinfo
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/filter from tailscale.com/types/netmap
tailscale.com/wgengine from tailscale.com/ipn
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/packet from tailscale.com/wgengine+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from golang.org/x/net/idna
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http
compress/zlib from image/png
compress/gzip from net/http+
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -148,7 +158,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
embed from tailscale.com/cmd/tailscale/cli+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -156,59 +169,51 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from tailscale.com/cmd/tailscale/cli+
errors from bufio+
expvar from tailscale.com/derp+
flag from github.com/peterbourgon/ff/v3+
flag from github.com/peterbourgon/ff/v2+
fmt from compress/flate+
hash from crypto+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate+
html/template from tailscale.com/cmd/tailscale/cli
image from github.com/skip2/go-qrcode+
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
hash/fnv from tailscale.com/wgengine/magicsock
html from tailscale.com/ipn/ipnstate
io from bufio+
io/fs from crypto/rand+
io/ioutil from golang.org/x/sys/cpu+
io/ioutil from crypto/tls+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from math/big+
mime from mime/multipart+
math/rand from github.com/mdlayher/netlink+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+
net/http/cgi from tailscale.com/cmd/tailscale/cli
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/netip from net
net/textproto from golang.org/x/net/http/httpguts+
net/textproto from mime/multipart+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/toqueteos/webbrowser+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from tailscale.com/util/groupmember
path from html/template+
L os/user from github.com/godbus/dbus/v5
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/tailscale/goupnp/httpu+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight+
LD runtime/cgo
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
text/template from html/template
text/template/parse from html/template+
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -8,20 +8,20 @@ package main // import "tailscale.com/cmd/tailscale"
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/apenwarr/fixconsole"
"tailscale.com/cmd/tailscale/cli"
)
func main() {
args := os.Args[1:]
args = cli.CleanUpArgs(args)
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
args = []string{"web", "-cgi"}
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Printf("fixConsoleOutput: %v\n", err)
}
if err := cli.Run(args); err != nil {
if err := cli.Run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

View File

@@ -1,20 +0,0 @@
// 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 childproc allows other packages to register "tailscaled be-child"
// child process hook code. This avoids duplicating build tags in the
// tailscaled package. Instead, the code that needs to fork/exec the self
// executable (when it's tailscaled) can instead register the code
// they want to run.
package childproc
var Code = map[string]func([]string) error{}
// Add registers code f to run as 'tailscaled be-child <typ> [args]'.
func Add(typ string, f func(args []string) error) {
if _, dup := Code[typ]; dup {
panic("dup hook " + typ)
}
Code[typ] = f
}

View File

@@ -1,318 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package main
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/portmapper"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
ifconfig bool // print network state once and exit
monitor bool
getURL string
derpCheck string
portmap bool
}
var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) > 0 {
return errors.New("unknown non-flag debug subcommand arguments")
}
ctx := context.Background()
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
if debugArgs.ifconfig {
return runMonitor(ctx, false)
}
if debugArgs.monitor {
return runMonitor(ctx, true)
}
if debugArgs.portmap {
return debugPortmap(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context, loop bool) error {
dump := func(st *interfaces.State) {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := monitor.New(log.Printf)
if err != nil {
return err
}
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Link monitor fired; no change")
return
}
log.Printf("Link monitor fired. New state:")
dump(st)
})
if loop {
log.Printf("Starting link change monitor; initial state:")
}
dump(mon.InterfaceState())
if !loop {
return nil
}
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
}
func getURL(ctx context.Context, urlStr string) error {
if urlStr == "login" {
urlStr = "https://login.tailscale.com"
}
log.SetOutput(os.Stdout)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
})
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
return fmt.Errorf("http.NewRequestWithContext: %v", err)
}
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
if err != nil {
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
}
log.Printf("proxy: %v", proxyURL)
tr := &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
ProxyConnectHeader: http.Header{},
DisableKeepAlives: true,
}
if proxyURL != nil {
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
if err == nil && auth != "" {
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
}
log.Printf("tshttpproxy.GetAuthHeader(%v) got: auth of %d bytes, err=%v", proxyURL, len(auth), err)
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
if auth != "" {
// We used log.Printf above (for timestamps).
// Use fmt.Printf here instead just to appease
// a security scanner, despite log.Printf only
// going to stdout.
fmt.Printf("... Proxy-Authorization = %q\n", auth)
}
}
res, err := tr.RoundTrip(req)
if err != nil {
return fmt.Errorf("Transport.RoundTrip: %v", err)
}
defer res.Body.Close()
return res.Write(os.Stdout)
}
func checkDerp(ctx context.Context, derpRegion string) error {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return fmt.Errorf("create derp map request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
}
var dmap tailcfg.DERPMap
if err = json.Unmarshal(b, &dmap); err != nil {
return fmt.Errorf("fetch DERP map: %w", err)
}
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
return r
}
}
for _, r := range dmap.Regions {
log.Printf("Known region: %q", r.RegionCode)
}
log.Fatalf("unknown region %q", derpRegion)
panic("unreachable")
}
priv1 := key.NewNode()
priv2 := key.NewNode()
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
c2.NotePreferred(true) // just to open it
m, err := c2.Recv()
log.Printf("c2 got %T, %v", m, err)
t0 := time.Now()
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
return err
}
fmt.Println(time.Since(t0))
m, err = c2.Recv()
log.Printf("c2 got %T, %v", m, err)
if err != nil {
return err
}
log.Printf("ok")
return err
}
func debugPortmap(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
logf := log.Printf
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
}
logf("cb: no mapping")
})
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
return err
}
gatewayAndSelfIP := func() (gw, self netaddr.IP, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netaddr.MustParseIP(v[:i])
self = netaddr.MustParseIP(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()
}
c.SetGatewayLookupFunc(gatewayAndSelfIP)
gw, selfIP, ok := gatewayAndSelfIP()
if !ok {
logf("no gateway or self IP; %v", linkMon.InterfaceState())
return nil
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
}
logf("Probe: %+v", res)
if !res.PCP && !res.PMP && !res.UPnP {
logf("no portmapping services available")
return nil
}
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {
logf("no mapping")
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -1,353 +1,176 @@
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
LD github.com/anmitsu/go-shlex from github.com/tailscale/ssh
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/ratelimit from github.com/aws/aws-sdk-go-v2/aws/retry
L github.com/aws/aws-sdk-go-v2/aws/retry from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client+
L github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4 from github.com/aws/aws-sdk-go-v2/aws/signer/v4
L github.com/aws/aws-sdk-go-v2/aws/signer/v4 from github.com/aws/aws-sdk-go-v2/service/internal/presigned-url+
L github.com/aws/aws-sdk-go-v2/aws/transport/http from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/config from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/credentials from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds
L github.com/aws/aws-sdk-go-v2/credentials/processcreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/credentials/ssocreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
L github.com/aws/aws-sdk-go-v2/service/internal/presigned-url from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/ssm from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/aws-sdk-go-v2/service/ssm/types from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress from github.com/klauspost/compress/zstd
L github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/snappy from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
W github.com/pkg/errors from github.com/tailscale/certstore
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
LD github.com/tailscale/ssh from tailscale.com/ssh/tailssh
github.com/pborman/getopt/v2 from tailscale.com/cmd/tailscaled
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
W 💣 golang.zx2c4.com/wireguard/ipc/namedpipe from golang.zx2c4.com/wireguard/ipc
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
inet.af/netaddr from inet.af/wf+
inet.af/peercred from tailscale.com/ipn/ipnserver
W 💣 inet.af/wf from tailscale.com/wf
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
tailscale.com from tailscale.com/version
💣 go4.org/mem from tailscale.com/control/controlclient+
inet.af/netaddr from tailscale.com/control/controlclient+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/ipn/ipnserver
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logtail from tailscale.com/logpolicy
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/filch from tailscale.com/logpolicy
💣 tailscale.com/metrics from tailscale.com/derp+
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
tailscale.com/net/interfaces from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netknob from tailscale.com/logpolicy+
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/paths from tailscale.com/client/tailscale+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/paths from tailscale.com/cmd/tailscaled+
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack
tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/cmd/tailscaled+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
LW tailscale.com/util/endian from tailscale.com/net/dns+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
tailscale.com/version from tailscale.com/client/tailscale+
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
tailscale.com/version from tailscale.com/control/controlclient+
tailscale.com/version/distro from tailscale.com/control/controlclient+
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
tailscale.com/wgengine/packet from tailscale.com/wgengine+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
tailscale.com/wgengine/wglog from tailscale.com/wgengine
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from github.com/tailscale/ssh+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
golang.org/x/crypto/ssh/terminal from tailscale.com/logpolicy
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
golang.org/x/sync/singleflight from tailscale.com/control/controlclient+
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from golang.org/x/net/idna
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/tcpip/stack+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
compress/gzip from internal/profile+
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
crypto/dsa from crypto/x509
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
embed from crypto/elliptic+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -355,46 +178,43 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
L flag from tailscale.com/net/netns
fmt from compress/flate+
hash from crypto+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+
hash/maphash from go4.org/mem
html from net/http/pprof+
hash/fnv from tailscale.com/wgengine/magicsock
html from html/template+
html/template from net/http/pprof
io from bufio+
io/fs from crypto/rand+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
io/ioutil from crypto/tls+
log from expvar+
LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
mime from mime/multipart+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/httputil from github.com/aws/smithy-go/transport/http+
net/http/internal from net/http+
net/http/pprof from tailscale.com/cmd/tailscaled+
net/netip from golang.zx2c4.com/wireguard/conn+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/http/internal from net/http
net/http/pprof from tailscale.com/cmd/tailscaled
net/textproto from mime/multipart+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscaled+
os/user from github.com/godbus/dbus/v5+
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
LD runtime/cgo
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
@@ -405,7 +225,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from crypto/x509+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -1,158 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
)
func init() {
installSystemDaemon = installSystemDaemonDarwin
uninstallSystemDaemon = uninstallSystemDaemonDarwin
}
// darwinLaunchdPlist is the launchd.plist that's written to
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
// future) a user-specific location.
//
// See man launchd.plist.
const darwinLaunchdPlist = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.tailscale.tailscaled</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/tailscaled</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
`
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
const targetBin = "/usr/local/bin/tailscaled"
const service = "com.tailscale.tailscaled"
func uninstallSystemDaemonDarwin(args []string) (ret error) {
if len(args) > 0 {
return errors.New("uninstall subcommand takes no arguments")
}
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
running := err == nil
if running {
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
if err != nil {
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
ret = err
}
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
if err != nil {
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
if ret == nil {
ret = err
}
}
}
if err := os.Remove(sysPlist); err != nil {
if os.IsNotExist(err) {
err = nil
}
if ret == nil {
ret = err
}
}
if err := os.Remove(targetBin); err != nil {
if os.IsNotExist(err) {
err = nil
}
if ret == nil {
ret = err
}
}
return ret
}
func installSystemDaemonDarwin(args []string) (err error) {
if len(args) > 0 {
return errors.New("install subcommand takes no arguments")
}
defer func() {
if err != nil && os.Getuid() != 0 {
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
}
}()
// Best effort:
uninstallSystemDaemonDarwin(nil)
// Copy ourselves to /usr/local/bin/tailscaled.
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
return err
}
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find our own executable path: %w", err)
}
tmpBin := targetBin + ".tmp"
f, err := os.Create(tmpBin)
if err != nil {
return err
}
self, err := os.Open(exe)
if err != nil {
f.Close()
return err
}
_, err = io.Copy(f, self)
self.Close()
if err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Chmod(tmpBin, 0755); err != nil {
return err
}
if err := os.Rename(tmpBin, targetBin); err != nil {
return err
}
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
}
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
}
return nil
}

View File

@@ -1,126 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"tailscale.com/logtail/backoff"
"tailscale.com/types/logger"
"tailscale.com/util/osshare"
)
func init() {
installSystemDaemon = installSystemDaemonWindows
uninstallSystemDaemon = uninstallSystemDaemonWindows
}
func installSystemDaemonWindows(args []string) (err error) {
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
}
service, err := m.OpenService(serviceName)
if err == nil {
service.Close()
return fmt.Errorf("service %q is already installed", serviceName)
}
// no such service; proceed to install the service.
exe, err := os.Executable()
if err != nil {
return err
}
c := mgr.Config{
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
StartType: mgr.StartAutomatic,
ErrorControl: mgr.ErrorNormal,
DisplayName: serviceName,
Description: "Connects this computer to others on the Tailscale network.",
}
service, err = m.CreateService(serviceName, exe, c)
if err != nil {
return fmt.Errorf("failed to create %q service: %v", serviceName, err)
}
defer service.Close()
// Exponential backoff is often too aggressive, so use (mostly)
// squares instead.
ra := []mgr.RecoveryAction{
{mgr.ServiceRestart, 1 * time.Second},
{mgr.ServiceRestart, 2 * time.Second},
{mgr.ServiceRestart, 4 * time.Second},
{mgr.ServiceRestart, 9 * time.Second},
{mgr.ServiceRestart, 16 * time.Second},
{mgr.ServiceRestart, 25 * time.Second},
{mgr.ServiceRestart, 36 * time.Second},
{mgr.ServiceRestart, 49 * time.Second},
{mgr.ServiceRestart, 64 * time.Second},
}
const resetPeriodSecs = 60
err = service.SetRecoveryActions(ra, resetPeriodSecs)
if err != nil {
return fmt.Errorf("failed to set service recovery actions: %v", err)
}
return nil
}
func uninstallSystemDaemonWindows(args []string) (ret error) {
// Remove file sharing from Windows shell (noop in non-windows)
osshare.SetFileSharingEnabled(false, logger.Discard)
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
}
defer m.Disconnect()
service, err := m.OpenService(serviceName)
if err != nil {
return fmt.Errorf("failed to open %q service: %v", serviceName, err)
}
st, err := service.Query()
if err != nil {
service.Close()
return fmt.Errorf("failed to query service state: %v", err)
}
if st.State != svc.Stopped {
service.Control(svc.Stop)
}
err = service.Delete()
service.Close()
if err != nil {
return fmt.Errorf("failed to delete service: %v", err)
}
bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
end := time.Now().Add(15 * time.Second)
for time.Until(end) > 0 {
service, err = m.OpenService(serviceName)
if err != nil {
// service is no longer openable; success!
break
}
service.Close()
bo.BackOff(context.Background(), errors.New("service not deleted"))
}
return nil
}

View File

@@ -1,82 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
// HTTP proxy code
package main
import (
"context"
"io"
"net"
"net/http"
"net/http/httputil"
"strings"
)
// httpProxyHandler returns an HTTP proxy http.Handler using the
// provided backend dialer.
func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler {
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {}, // no change
Transport: &http.Transport{
DialContext: dialer,
},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "CONNECT" {
backURL := r.RequestURI
if strings.HasPrefix(backURL, "/") || backURL == "*" {
http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400)
return
}
rp.ServeHTTP(w, r)
return
}
// CONNECT support:
dst := r.RequestURI
c, err := dialer(r.Context(), "tcp", dst)
if err != nil {
w.Header().Set("Tailscale-Connect-Error", err.Error())
http.Error(w, err.Error(), 500)
return
}
defer c.Close()
cc, ccbuf, err := w.(http.Hijacker).Hijack()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer cc.Close()
io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n")
var clientSrc io.Reader = ccbuf
if ccbuf.Reader.Buffered() == 0 {
// In the common case (with no
// buffered data), read directly from
// the underlying client connection to
// save some memory, letting the
// bufio.Reader/Writer get GC'ed.
clientSrc = cc
}
errc := make(chan error, 1)
go func() {
_, err := io.Copy(cc, c)
errc <- err
}()
go func() {
_, err := io.Copy(c, clientSrc)
errc <- err
}()
<-errc
})
}

View File

@@ -1,12 +0,0 @@
// 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.
//go:build !go1.18
// +build !go1.18
package main
func init() {
you_need_Go_1_18_to_compile_Tailscale()
}

View File

@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
// The tailscaled program is the Tailscale client daemon. It's configured
// and controlled via the tailscale CLI program.
//
@@ -14,53 +11,36 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"syscall"
"time"
"inet.af/netaddr"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn"
"github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tsweb"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
)
// globalStateKey is the ipn.StateKey that tailscaled loads on
// startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
const globalStateKey = "_daemon"
// defaultTunName returns the default tun device name for the platform.
func defaultTunName() string {
switch runtime.GOOS {
@@ -68,65 +48,18 @@ func defaultTunName() string {
return "tun"
case "windows":
return "Tailscale"
case "darwin":
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
// as a magic value that uses/creates any free number.
return "utun"
case "linux":
switch distro.Get() {
case distro.Synology:
// Try TUN, but fall back to userspace networking if needed.
// See https://github.com/tailscale/tailscale-synology/issues/35
return "tailscale0,userspace-networking"
case distro.Gokrazy:
// Gokrazy doesn't yet work in tun mode because the whole
// Gokrazy thing is no C code, and Tailscale currently
// depends on the iptables binary for Linux's
// wgengine/router.
// But on Gokrazy there's no legacy iptables, so we could use netlink
// to program nft-iptables directly. It just isn't done yet;
// see https://github.com/tailscale/tailscale/issues/391
//
// But Gokrazy does have the tun module built-in, so users
// can stil run --tun=tailscale0 if they wish, if they
// arrange for iptables to be present or run in "tailscale
// up --netfilter-mode=off" mode, perhaps. Untested.
return "userspace-networking"
}
}
return "tailscale0"
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
// or comma-separated list thereof.
tunname string
cleanup bool
debug string
port uint16
statepath string
statedir string
socketpath string
birdSocketPath string
verbose int
socksAddr string // listen address for SOCKS5 server
httpProxyAddr string // listen address for HTTP proxy server
}
var (
installSystemDaemon func([]string) error // non-nil on some platforms
uninstallSystemDaemon func([]string) error // non-nil on some platforms
createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms
)
var subCommands = map[string]*func([]string) error{
"install-system-daemon": &installSystemDaemon,
"uninstall-system-daemon": &uninstallSystemDaemon,
"debug": &debugModeFunc,
"be-child": &beChildFunc,
cleanup bool
fake bool
debug string
tunname string
port uint16
statepath string
socketpath string
}
func main() {
@@ -138,159 +71,48 @@ func main() {
debug.SetGCPercent(10)
}
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")
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
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.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
// Set default values for getopt.
args.tunname = defaultTunName()
args.port = magicsock.DefaultPort
args.statepath = paths.DefaultTailscaledStateFile()
args.socketpath = paths.DefaultTailscaledSocket()
if len(os.Args) > 1 {
sub := os.Args[1]
if fp, ok := subCommands[sub]; ok {
if *fp == nil {
log.SetFlags(0)
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
}
if err := (*fp)(os.Args[2:]); err != nil {
log.SetFlags(0)
log.Fatal(err)
}
return
}
getopt.FlagLong(&args.cleanup, "cleanup", 0, "clean up system state and exit")
getopt.FlagLong(&args.fake, "fake", 0, "fake tunnel+routing instead of tuntap")
getopt.FlagLong(&args.debug, "debug", 0, "address of debug server")
getopt.FlagLong(&args.tunname, "tun", 0, "tunnel interface name")
getopt.FlagLong(&args.port, "port", 'p', "WireGuard port (0=autoselect)")
getopt.FlagLong(&args.statepath, "state", 0, "path of state file")
getopt.FlagLong(&args.socketpath, "socket", 's', "path of the service unix socket")
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Fatalf("fixConsoleOutput: %v", err)
}
if beWindowsSubprocess() {
return
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
flag.Parse()
if flag.NArg() > 0 {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
}
if printVersion {
fmt.Println(version.String())
os.Exit(0)
}
if runtime.GOOS == "darwin" && runtime.Version() == "go1.18" {
log.Fatalf("tailscaled is broken on macOS with go1.18 due to upstream bug https://github.com/golang/go/issues/51759; use 1.18.1+ or Tailscale's Go fork")
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
if args.statepath == "" {
log.Fatalf("--state is required")
}
if args.socketpath == "" && runtime.GOOS != "windows" {
log.SetFlags(0)
log.Fatalf("--socket is required")
}
if args.birdSocketPath != "" && createBIRDClient == nil {
log.SetFlags(0)
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
if err := run(); err != nil {
// No need to log; the func already did
os.Exit(1)
}
err := run()
// Remove file sharing from Windows shell (noop in non-windows)
osshare.SetFileSharingEnabled(false, logger.Discard)
if err != nil {
log.Fatal(err)
}
}
func trySynologyMigration(p string) error {
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
return nil
}
fi, err := os.Stat(p)
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
return err
}
// File is empty or doesn't exist, try reading from the old path.
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
if _, err := os.Stat(oldPath); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
return err
}
if err := os.Rename(oldPath, p); err != nil {
return err
}
return nil
}
func statePathOrDefault() string {
if args.statepath != "" {
return args.statepath
}
if args.statedir != "" {
return filepath.Join(args.statedir, "tailscaled.state")
}
return ""
}
func ipnServerOpts() (o ipnserver.Options) {
// Allow changing the OS-specific IPN behavior for tests
// so we can e.g. test Windows-specific behaviors on Linux.
goos := envknob.String("TS_DEBUG_TAILSCALED_IPN_GOOS")
if goos == "" {
goos = runtime.GOOS
}
o.VarRoot = args.statedir
// If an absolute --state is provided but not --statedir, try to derive
// a state directory.
if o.VarRoot == "" && filepath.IsAbs(args.statepath) {
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") {
o.VarRoot = dir
}
}
if strings.HasPrefix(statePathOrDefault(), "mem:") {
// Register as an ephemeral node.
o.LoginFlags = controlclient.LoginEphemeral
}
switch goos {
case "js":
// The js/wasm client has no state storage so for now
// treat all interactive logins as ephemeral.
// TODO(bradfitz): if we start using browser LocalStorage
// or something, then rethink this.
o.LoginFlags = controlclient.LoginEphemeral
fallthrough
default:
o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey
case "windows":
// Not those.
}
return o
}
func run() error {
var err error
pol := logpolicy.New(logtail.CollectionNode)
pol.SetVerbosityLevel(args.verbose)
pol := logpolicy.New("tailnode.log.tailscale.io")
defer func() {
// Finish uploading logs after closing everything else.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
@@ -298,102 +120,30 @@ func run() error {
pol.Shutdown(ctx)
}()
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
if err := runWindowsService(pol); err != nil {
log.Printf("runservice: %v", err)
}
log.Printf("Service ended.")
return nil
}
var logf logger.Logf = log.Printf
if envknob.Bool("TS_DEBUG_MEMORY") {
logf = logger.RusagePrefixLog(logf)
}
logf := wgengine.RusagePrefixLog(log.Printf)
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
if envknob.Bool("TS_PLEASE_PANIC") {
panic("TS_PLEASE_PANIC asked us to panic")
}
dns.Cleanup(logf, args.tunname)
router.Cleanup(logf, args.tunname)
return nil
}
if args.statepath == "" && args.statedir == "" {
log.Fatalf("--statedir (or at least --state) is required")
}
if err := trySynologyMigration(statePathOrDefault()); err != nil {
log.Printf("error in synology migration: %v", err)
}
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()
}
linkMon, err := monitor.New(logf)
if err != nil {
return fmt.Errorf("monitor.New: %w", err)
}
pol.Logtail.SetLinkMonitor(linkMon)
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := new(tsdial.Dialer) // mutated below (before used)
e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return fmt.Errorf("createEngine: %w", err)
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
if debugMux != nil {
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
}
go runDebugServer(debugMux, args.debug)
}
ns, err := newNetstack(logf, dialer, e)
var e wgengine.Engine
if args.fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
} else {
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
}
if err != nil {
return fmt.Errorf("newNetstack: %w", err)
logf("wgengine.New: %v", err)
return err
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
if useNetstack {
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := e.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
}
if socksListener != nil || httpProxyListener != nil {
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
}
if socksListener != nil {
ss := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
Dialer: dialer.UserDial,
}
go func() {
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
}()
}
}
e = wgengine.NewWatchdog(e)
ctx, cancel := context.WithCancel(context.Background())
@@ -415,142 +165,27 @@ func run() error {
}
}()
opts := ipnServerOpts()
store, err := store.New(logf, statePathOrDefault())
if err != nil {
return fmt.Errorf("store.New: %w", err)
opts := ipnserver.Options{
SocketPath: args.socketpath,
Port: 41112,
StatePath: args.statepath,
AutostartStateKey: globalStateKey,
LegacyConfigPath: paths.LegacyConfigPath(),
SurviveDisconnects: true,
DebugMux: debugMux,
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
if err != nil {
return fmt.Errorf("ipnserver.New: %w", err)
}
ns.SetLocalBackend(srv.LocalBackend())
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = srv.Run(ctx, ln)
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
return fmt.Errorf("ipnserver.Run: %w", err)
logf("ipnserver.Run: %v", err)
return err
}
return nil
}
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
if args.tunname == "" {
return nil, false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
if err == nil {
return e, useNetstack, nil
}
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
errs = append(errs, err)
}
return nil, false, multierr.New(errs...)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
return v
}
if distro.Get() == distro.Synology {
return true
}
switch runtime.GOOS {
case "windows", "darwin", "freebsd":
// Enable on Windows and tailscaled-on-macOS (this doesn't
// affect the GUI clients), and on FreeBSD.
return true
}
return false
}
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
}
useNetstack = name == "userspace-networking"
netns.SetEnabled(!useNetstack)
if args.birdSocketPath != "" && createBIRDClient != nil {
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
if err != nil {
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
}
}
if useNetstack {
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
// On Synology in netstack mode, still init a DNS
// manager (directManager) to avoid the health check
// warnings in 'tailscale status' about DNS base
// configuration being unavailable (from the noop
// manager). More in Issue 4017.
// TODO(bradfitz): add a Synology-specific DNS manager.
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
if err != nil {
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
}
} else {
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev
if strings.HasPrefix(name, "tap:") {
conf.IsTAP = true
e, err := wgengine.NewUserspaceEngine(logf, conf)
return e, false, err
}
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, fmt.Errorf("creating router: %w", err)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
conf.DNS = d
conf.Router = r
if wrapNetstack {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}
e, err = wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
return nil, useNetstack, err
}
return e, useNetstack, nil
}
func newDebugMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/debug/metrics", servePrometheusMetrics)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
@@ -559,12 +194,6 @@ func newDebugMux() *http.ServeMux {
return mux
}
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
tsweb.VarzHandler(w, r)
clientmetric.WritePrometheusExpositionFormat(w)
}
func runDebugServer(mux *http.ServeMux, addr string) {
srv := &http.Server{
Addr: addr,
@@ -574,69 +203,3 @@ func runDebugServer(mux *http.ServeMux, addr string) {
log.Fatal(err)
}
}
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn, dialer)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
// proxies, if the respective addresses are not empty. socksAddr and
// httpAddr can be the same, in which case socksListener will receive
// connections that look like they're speaking SOCKS and httpListener
// will receive everything else.
//
// socksListener and httpListener can be nil, if their respective
// addrs are empty.
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
ln, err := net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("proxy listener: %v", err)
}
return proxymux.SplitSOCKSAndHTTP(ln)
}
var err error
if socksAddr != "" {
socksListener, err = net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("SOCKS5 listener: %v", err)
}
if strings.HasSuffix(socksAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
}
}
if httpAddr != "" {
httpListener, err = net.Listen("tcp", httpAddr)
if err != nil {
log.Fatalf("HTTP proxy listener: %v", err)
}
if strings.HasSuffix(httpAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
}
}
return socksListener, httpListener
}
var beChildFunc = beChild
func beChild(args []string) error {
if len(args) == 0 {
return errors.New("missing mode argument")
}
typ := args[0]
f, ok := childproc.Code[typ]
if !ok {
return fmt.Errorf("unknown be-child mode %q", typ)
}
return f(args[1:])
}

View File

@@ -1,25 +0,0 @@
#!/sbin/openrc-run
set -a
source /etc/default/tailscaled
set +a
command="/usr/sbin/tailscaled"
command_args="--state=/var/lib/tailscale/tailscaled.state --port=$PORT --socket=/var/run/tailscale/tailscaled.sock $FLAGS"
command_background=true
pidfile="/run/tailscaled.pid"
start_stop_daemon_args="-1 /var/log/tailscaled.log -2 /var/log/tailscaled.log"
depend() {
need net
}
start_pre() {
mkdir -p /var/run/tailscale
mkdir -p /var/lib/tailscale
$command --cleanup
}
stop_post() {
$command --cleanup
}

View File

@@ -2,11 +2,10 @@
Description=Tailscale node agent
Documentation=https://tailscale.com/kb/
Wants=network-pre.target
After=network-pre.target NetworkManager.service systemd-resolved.service
After=network-pre.target
[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
ExecStopPost=/usr/sbin/tailscaled --cleanup
@@ -15,10 +14,9 @@ Restart=on-failure
RuntimeDirectory=tailscale
RuntimeDirectoryMode=0755
StateDirectory=tailscale
StateDirectoryMode=0700
StateDirectoryMode=0750
CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
[Install]
WantedBy=multi-user.target

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18 && (linux || darwin || freebsd || openbsd)
// +build go1.18
// +build linux darwin freebsd openbsd
package main
import (
"tailscale.com/chirp"
"tailscale.com/wgengine"
)
func init() {
createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) {
return chirp.New(ctlSocket)
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows && go1.18
// +build !windows,go1.18
package main // import "tailscale.com/cmd/tailscaled"
import "tailscale.com/logpolicy"
func isWindowsService() bool { return false }
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
func beWindowsSubprocess() bool { return false }

View File

@@ -1,13 +0,0 @@
// 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 main // import "tailscale.com/cmd/tailscaled"
import "testing"
func TestNothing(t *testing.T) {
// This test does nothing on purpose, so we can run
// GODEBUG=memprofilerate=1 go test -v -run=Nothing -memprofile=prof.mem
// without any errors about no matching tests.
}

View File

@@ -1,381 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
// +build go1.18
package main // import "tailscale.com/cmd/tailscaled"
// TODO: check if administrator, like tswin does.
//
// TODO: try to load wintun.dll early at startup, before wireguard/tun
// does (which panics) and if we'd fail (e.g. due to access
// denied, even if administrator), use 'tasklist /m wintun.dll'
// to see if something else is currently using it and tell user.
//
// TODO: check if Tailscale service is already running, and fail early
// like tswin does.
//
// TODO: on failure, check if on a UNC drive and recommend copying it
// to C:\ to run it, like tswin does.
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/safesocket"
"tailscale.com/types/logger"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
const serviceName = "Tailscale"
func isWindowsService() bool {
v, err := svc.IsWindowsService()
if err != nil {
log.Fatalf("svc.IsWindowsService failed: %v", err)
}
return v
}
// runWindowsService starts running Tailscale under the Windows
// Service environment.
//
// At this point we're still the parent process that
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
return svc.Run(serviceName, &ipnService{Policy: pol})
}
type ipnService struct {
Policy *logpolicy.Policy
}
// Called by Windows to execute the windows service.
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
svcAccepts := svc.AcceptStop
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
svcAccepts |= svc.AcceptSessionChange
}
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
args := []string{"/subproc", service.Policy.PublicID.String()}
// Make a logger without a date prefix, as filelogger
// and logtail both already add their own. All we really want
// from the log package is the automatic newline.
// We start with log.Default().Writer(), which is the logtail
// writer that logpolicy already installed as the global
// output.
logger := log.New(log.Default().Writer(), "", 0)
ipnserver.BabysitProc(ctx, args, logger.Printf)
}()
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
for ctx.Err() == nil {
select {
case <-doneCh:
case cmd := <-r:
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
switch cmd.Cmd {
case svc.Stop:
cancel()
case svc.Interrogate:
changes <- cmd.CurrentStatus
case svc.SessionChange:
handleSessionChange(cmd)
changes <- cmd.CurrentStatus
}
}
}
changes <- svc.Status{State: svc.StopPending}
return false, windows.NO_ERROR
}
func cmdName(c svc.Cmd) string {
switch c {
case svc.Stop:
return "Stop"
case svc.Pause:
return "Pause"
case svc.Continue:
return "Continue"
case svc.Interrogate:
return "Interrogate"
case svc.Shutdown:
return "Shutdown"
case svc.ParamChange:
return "ParamChange"
case svc.NetBindAdd:
return "NetBindAdd"
case svc.NetBindRemove:
return "NetBindRemove"
case svc.NetBindEnable:
return "NetBindEnable"
case svc.NetBindDisable:
return "NetBindDisable"
case svc.DeviceEvent:
return "DeviceEvent"
case svc.HardwareProfileChange:
return "HardwareProfileChange"
case svc.PowerEvent:
return "PowerEvent"
case svc.SessionChange:
return "SessionChange"
case svc.PreShutdown:
return "PreShutdown"
}
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
}
func beWindowsSubprocess() bool {
if beFirewallKillswitch() {
return true
}
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file logggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("stdin err (parent process died): %v", err)
}
}
}()
err := startIPNServer(context.Background(), logid)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}
return true
}
func beFirewallKillswitch() bool {
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
return false
}
log.SetFlags(0)
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
guid, err := windows.GUIDFromString(os.Args[2])
if err != nil {
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
}
luid, err := winipcfg.LUIDFromGUID(&guid)
if err != nil {
log.Fatalf("no interface with GUID %q: %v", guid, err)
}
start := time.Now()
fw, err := wf.New(uint64(luid))
if err != nil {
log.Fatalf("failed to enable firewall: %v", err)
}
log.Printf("killswitch enabled, took %s", time.Since(start))
// Note(maisem): when local lan access toggled, tailscaled needs to
// inform the firewall to let local routes through. The set of routes
// is passed in via stdin encoded in json.
dcd := json.NewDecoder(os.Stdin)
for {
var routes []netaddr.IPPrefix
if err := dcd.Decode(&routes); err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
if err := fw.UpdatePermittedRoutes(routes); err != nil {
log.Fatalf("failed to update routes (%v)", err)
}
}
}
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
linkMon, err := monitor.New(logf)
if err != nil {
return err
}
dialer := new(tsdial.Dialer)
getEngineRaw := func() (wgengine.Engine, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
return nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev, nil)
if err != nil {
dev.Close()
return nil, fmt.Errorf("router: %w", err)
}
if wrapNetstack {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("DNS: %w", err)
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
LinkMonitor: linkMon,
Dialer: dialer,
})
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("engine: %w", err)
}
ns, err := newNetstack(logf, dialer, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
if err := ns.Start(); err != nil {
return nil, fmt.Errorf("failed to start netstack: %w", err)
}
return wgengine.NewWatchdog(eng), nil
}
type engineOrError struct {
Engine wgengine.Engine
Err error
}
engErrc := make(chan engineOrError)
t0 := time.Now()
go func() {
const ms = time.Millisecond
for try := 1; ; try++ {
logf("tailscaled: getting engine... (try %v)", try)
t1 := time.Now()
eng, err := getEngineRaw()
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
if err != nil {
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
try, d, dt, windowsUptime().Round(time.Second), err)
} else {
if try > 1 {
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
} else {
logf("tailscaled: got engine in %v", d)
}
}
timer := time.NewTimer(5 * time.Second)
engErrc <- engineOrError{eng, err}
if err == nil {
timer.Stop()
return
}
<-timer.C
}
}()
// getEngine is called by ipnserver to get the engine. It's
// not called concurrently and is not called again once it
// successfully returns an engine.
getEngine := func() (wgengine.Engine, error) {
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
}
for {
res := <-engErrc
if res.Engine != nil {
return res.Engine, nil
}
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
// Ignore errors during early boot. Windows 10 auto logs in the GUI
// way sooner than the networking stack components start up.
// So the network will fail for a bit (and require a few tries) while
// the GUI is still fine.
continue
}
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
}
}
store, err := store.New(logf, statePathOrDefault())
if err != nil {
return err
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
if err != nil {
logf("ipnserver.Run: %v", err)
}
return err
}
func handleSessionChange(chgRequest svc.ChangeRequest) {
if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
return
}
log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
go func() {
err := dns.Flush()
if err != nil {
log.Printf("Error flushing DNS on session unlock: %v", err)
}
}()
}
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
)
func windowsUptime() time.Duration {
r, _, _ := getTickCount64Proc.Call()
return time.Duration(int64(r)) * time.Millisecond
}

View File

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

View File

@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
// The tsshd binary is an SSH server that accepts connections
@@ -30,12 +29,10 @@ import (
"time"
"unsafe"
"github.com/creack/pty"
"github.com/tailscale/ssh"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
)
var (
@@ -60,11 +57,11 @@ func main() {
warned := false
for {
addrs, iface, err := interfaces.Tailscale()
addr, iface, err := interfaces.Tailscale()
if err != nil {
log.Fatalf("listing interfaces: %v", err)
}
if len(addrs) == 0 {
if addr == nil {
if !warned {
log.Printf("no tailscale interface found; polling until one is available")
warned = true
@@ -76,13 +73,6 @@ func main() {
continue
}
warned = false
var addr netaddr.IP
for _, a := range addrs {
if a.Is4() {
addr = a
break
}
}
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
s := &ssh.Server{
@@ -106,13 +96,7 @@ func handleSSH(s ssh.Session) {
s.Exit(1)
return
}
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
if !ok {
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
s.Exit(1)
return
}
if !tsaddr.IsTailscaleIP(tanetaddr) {
if !interfaces.IsTailscaleIP(ta.IP) {
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return
@@ -157,9 +141,8 @@ func handleSSH(s ssh.Session) {
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
} else {
s.Exit(0)
}
s.Exit(0)
return
}

View File

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

View File

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

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