Compare commits
1 Commits
v1.24.2
...
dsnet/admi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f0753be86 |
@@ -1 +0,0 @@
|
||||
suppress_failure_on_regression: true
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1 @@
|
||||
go.mod filter=go-mod
|
||||
*.go diff=golang
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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!
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
---
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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!
|
||||
21
.github/dependabot.yml
vendored
21
.github/dependabot.yml
vendored
@@ -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:"
|
||||
26
.github/workflows/cifuzz.yml
vendored
26
.github/workflows/cifuzz.yml
vendored
@@ -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@v3
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@@ -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@v3
|
||||
|
||||
# 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
|
||||
12
.github/workflows/cross-darwin.yml
vendored
12
.github/workflows/cross-darwin.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: macOS build cmd
|
||||
env:
|
||||
@@ -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: |
|
||||
|
||||
6
.github/workflows/cross-freebsd.yml
vendored
6
.github/workflows/cross-freebsd.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: FreeBSD build cmd
|
||||
env:
|
||||
|
||||
6
.github/workflows/cross-openbsd.yml
vendored
6
.github/workflows/cross-openbsd.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: OpenBSD build cmd
|
||||
env:
|
||||
|
||||
6
.github/workflows/cross-windows.yml
vendored
6
.github/workflows/cross-windows.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Windows build cmd
|
||||
env:
|
||||
|
||||
6
.github/workflows/depaware.yml
vendored
6
.github/workflows/depaware.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: depaware tailscaled
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
|
||||
16
.github/workflows/go_generate.yml
vendored
16
.github/workflows/go_generate.yml
vendored
@@ -15,24 +15,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
if [[ "${{github.ref}}" == release-branch/* ]]
|
||||
then
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
else
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
fi
|
||||
go generate $pkgs
|
||||
mkdir gentools
|
||||
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
|
||||
PATH="$PATH:$(pwd)/gentools" go generate ./...
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
||||
|
||||
6
.github/workflows/license.yml
vendored
6
.github/workflows/license.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run license checker
|
||||
run: ./scripts/check_license_headers.sh .
|
||||
|
||||
21
.github/workflows/linux-race.yml
vendored
21
.github/workflows/linux-race.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
@@ -31,21 +31,6 @@ jobs:
|
||||
- name: Run tests and benchmarks with -race flag on linux
|
||||
run: go test -race -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
30
.github/workflows/linux.yml
vendored
30
.github/workflows/linux.yml
vendored
@@ -17,44 +17,20 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Get QEMU
|
||||
run: |
|
||||
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
|
||||
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
|
||||
# use this PPA which brings in a modern qemu.
|
||||
sudo add-apt-repository -y ppa:jacob/virtualisation
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
21
.github/workflows/linux32.yml
vendored
21
.github/workflows/linux32.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Basic build
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
@@ -31,21 +31,6 @@ jobs:
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
26
.github/workflows/staticcheck.yml
vendored
26
.github/workflows/staticcheck.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
@@ -31,28 +31,16 @@ jobs:
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: "386"
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
27
.github/workflows/windows-race.yml
vendored
27
.github/workflows/windows-race.yml
vendored
@@ -17,31 +17,20 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.16.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
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') }}
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test with -race flag
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
|
||||
24
.github/workflows/windows.yml
vendored
24
.github/workflows/windows.yml
vendored
@@ -17,28 +17,20 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.16.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
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') }}
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
name: VM
|
||||
name: "integration-vms"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- "tstest/integration/vms/**"
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
ubuntu2004-LTS-cloud-base:
|
||||
runs-on: [ self-hosted, linux, vm ]
|
||||
experimental-linux-vm-test:
|
||||
# To set up a new runner, see tstest/integration/vms/runner.nix
|
||||
runs-on: [ self-hosted, linux, vm_integration_test ]
|
||||
|
||||
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@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download VM Images
|
||||
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m -no-s3
|
||||
env:
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
|
||||
run: go test ./tstest/integration/vms -v -run-vm-tests
|
||||
env:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,7 +5,6 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.spk
|
||||
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -4,11 +4,17 @@
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# 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.
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# See current bugs tagged "containers":
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
@@ -17,11 +23,11 @@
|
||||
#
|
||||
# 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,7 +38,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
FROM golang:1.16-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -48,13 +54,13 @@ ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN GOARCH=$TARGETARCH go install -ldflags="\
|
||||
RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled
|
||||
-v ./cmd/...
|
||||
|
||||
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/
|
||||
|
||||
@@ -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
|
||||
43
Makefile
43
Makefile
@@ -1,49 +1,24 @@
|
||||
IMAGE_REPO ?= tailscale/tailscale
|
||||
SYNO_ARCH ?= "amd64"
|
||||
SYNO_DSM ?= "7"
|
||||
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
vet:
|
||||
./tool/go vet ./...
|
||||
|
||||
tidy:
|
||||
./tool/go mod tidy -compat=1.17
|
||||
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
|
||||
GOOS=windows GOARCH=amd64 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
|
||||
GOOS=linux GOARCH=386 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 buildwindows build386
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -44,7 +43,7 @@ If your distro has conventions that preclude the use of
|
||||
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.16) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.24.2
|
||||
1.13.0
|
||||
|
||||
313
api.md
313
api.md
@@ -3,7 +3,7 @@
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
|
||||
# APIs
|
||||
|
||||
@@ -13,25 +13,13 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
- Authorize machine
|
||||
- [POST device authorized](#device-authorized-post)
|
||||
- Tags
|
||||
- [POST device tags](#device-tags-post)
|
||||
- Key
|
||||
- [POST device key](#device-key-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [Keys](#tailnet-keys)
|
||||
- [GET tailnet keys](#tailnet-keys-get)
|
||||
- [POST tailnet key](#tailnet-keys-post)
|
||||
- [GET tailnet key](#tailnet-keys-key-get)
|
||||
- [DELETE tailnet key](#tailnet-keys-key-delete)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
@@ -49,7 +37,7 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></a>
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
@@ -138,7 +126,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></a>
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
@@ -175,7 +163,7 @@ If the device is not owned by your tailnet:
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></a>
|
||||
<a name=device-routes-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
@@ -204,7 +192,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></a>
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
@@ -245,100 +233,6 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-authorized-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/authorized` - authorize a device
|
||||
|
||||
Marks a device as authorized, for Tailnets where device authorization is required.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`authorized` - whether the device is authorized; only `true` is currently supported.
|
||||
```
|
||||
{
|
||||
"authorized": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/authorized' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"authorized": true}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-tags-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/tags` - update tags on a device
|
||||
|
||||
Updates the tags set on a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`tags` - The new list of tags for the device.
|
||||
|
||||
```
|
||||
{
|
||||
"tags": ["tag:foo", "tag:bar"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/tags' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"tags": ["tag:foo", "tag:bar"]}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-key-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/key` - update device key
|
||||
|
||||
Allows for updating properties on the device key.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`keyExpiryDisabled`
|
||||
|
||||
- Provide `true` to disable the device's key expiry. The original key expiry time is still maintained. Upon re-enabling, the key will expire at that original time.
|
||||
- Provide `false` to enable the device's key expiry. Sets the key to expire at the original expiry time prior to disabling. The key may already have expired. In that case, the device must be re-authenticated.
|
||||
- Empty value will not change the key expiry.
|
||||
|
||||
`preauthorized`
|
||||
|
||||
- If `true`, don't require machine authorization (if enabled on the tailnet)
|
||||
|
||||
```
|
||||
{
|
||||
"keyExpiryDisabled": true,
|
||||
"preauthorized": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/key' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"keyExpiryDisabled": true}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
## Tailnet
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
@@ -579,7 +473,7 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
###### Query Parameters
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
The provided ACL is queried with this paramater to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
@@ -616,48 +510,6 @@ Response:
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-validate-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
|
||||
|
||||
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The POST body should be a JSON formatted array of ACL Tests.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
[
|
||||
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
|
||||
]'
|
||||
```
|
||||
|
||||
Response:
|
||||
If all the tests pass, the response will be empty, with an http status code of 200.
|
||||
|
||||
Failed test error response:
|
||||
A 400 http status code and the errors in the response body.
|
||||
```
|
||||
{
|
||||
"message":"test(s) failed",
|
||||
"data":[
|
||||
{
|
||||
"user":"user1@example.com",
|
||||
"errors":["address \"2.2.2.2:22\": want: Drop, got: Accept"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
@@ -744,159 +596,6 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys></a>
|
||||
|
||||
### Keys
|
||||
|
||||
<a name=tailnet-keys-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys` - list the keys for a tailnet
|
||||
|
||||
Returns a list of active keys for a tailnet
|
||||
for the user who owns the API key used to perform this query.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the IDs of all active keys.
|
||||
This includes both API keys and also machine authentication keys.
|
||||
In the future, this may provide more information about each key than just the ID.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{"keys": [
|
||||
{"id": "kYKVU14CNTRL"},
|
||||
{"id": "k68VdZ3CNTRL"},
|
||||
{"id": "kJ9nq43CNTRL"},
|
||||
{"id": "kkThgj1CNTRL"}
|
||||
]}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/keys` - create a new key for a tailnet
|
||||
|
||||
Create a new key in a tailnet associated
|
||||
with the user who owns the API key used to perform this request.
|
||||
Supply the tailnet in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the provided capabilities in addition to the
|
||||
generated key. The key should be recorded and kept safe and secure as it
|
||||
wields the capabilities specified in the request. The identity of the key
|
||||
is embedded in the key itself and can be used to perform operations on
|
||||
the key (e.g., revoking it or retrieving information about it).
|
||||
The full key can no longer be retrieved by the server.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
echo '{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | curl -X POST --data-binary @- https://api.tailscale.com/api/v2/tailnet/example.com/keys \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Content-Type: application/json" | jsonfmt
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"created": "2021-12-09T23:22:39Z",
|
||||
"expires": "2022-03-09T23:22:39Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys/:keyid` - get information for a specific key
|
||||
|
||||
Returns a JSON object with information about specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with information about the key such as
|
||||
when it was created and when it expires.
|
||||
It also lists the capabilities associated with the key.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"created": "2021-12-09T22:13:53Z",
|
||||
"expires": "2022-03-09T22:13:53Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/tailnet/:tailnet/keys/:keyid` - delete a specific key
|
||||
|
||||
Deletes a specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
This reports status 200 upon success.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
@@ -30,14 +30,12 @@ else
|
||||
fi
|
||||
|
||||
long_suffix="$change_suffix-t$short_hash"
|
||||
MINOR="$major.$minor"
|
||||
SHORT="$MINOR.$patch"
|
||||
SHORT="$major.$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"
|
||||
@@ -45,4 +43,4 @@ 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}" "$@"
|
||||
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
|
||||
@@ -8,39 +8,27 @@
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# 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.
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# See current bugs tagged "containers":
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various 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}"
|
||||
docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:tailscale .
|
||||
|
||||
129
chirp/chirp.go
129
chirp/chirp.go
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,6 @@ import "tailscale.com/tailcfg"
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
UserProfile *tailcfg.UserProfile
|
||||
|
||||
// Caps are extra capabilities that the remote Node has to this node.
|
||||
Caps []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||
|
||||
@@ -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("", ""))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -2,16 +2,12 @@
|
||||
// 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"
|
||||
@@ -19,67 +15,42 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"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/net/netutil"
|
||||
"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()
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var 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)
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -90,59 +61,16 @@ var (
|
||||
//
|
||||
// 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 {
|
||||
@@ -153,29 +81,12 @@ func bestError(err error, body []byte) 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)
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +96,7 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != wantStatus {
|
||||
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
|
||||
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
|
||||
return nil, bestError(err, slurp)
|
||||
}
|
||||
return slurp, nil
|
||||
@@ -216,24 +127,6 @@ 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)
|
||||
@@ -243,22 +136,12 @@ func BugReport(ctx context.Context, note string) (string, error) {
|
||||
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.
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
@@ -275,21 +158,6 @@ func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// IDToken is a request to get an OIDC ID token for an audience.
|
||||
// The token can be presented to any resource provider which offers OIDC
|
||||
// Federation.
|
||||
func IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr := new(tailcfg.TokenResponse)
|
||||
if err := json.Unmarshal(body, tr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/files/")
|
||||
if err != nil {
|
||||
@@ -312,7 +180,7 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -340,30 +208,6 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
|
||||
return fts, nil
|
||||
}
|
||||
|
||||
// PushFile sends Taildrop file r to target.
|
||||
//
|
||||
// A size of -1 means unknown.
|
||||
// The name parameter is the original filename, not escaped.
|
||||
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size != -1 {
|
||||
req.ContentLength = size
|
||||
}
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
return 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 {
|
||||
@@ -381,21 +225,6 @@ func CheckIPForwarding(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPrefs validates the provided preferences, without making any changes.
|
||||
//
|
||||
// The CLI uses this before a Start call to fail fast if the preferences won't
|
||||
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
|
||||
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
|
||||
// EditPrefs is not necessary.
|
||||
func CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
|
||||
pj, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/prefs")
|
||||
if err != nil {
|
||||
@@ -451,60 +280,6 @@ func SetDNS(ctx context.Context, name, value string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DialTCP connects to the host's port via Tailscale.
|
||||
//
|
||||
// The host may be a base DNS name (resolved from the netmap inside
|
||||
// tailscaled), a FQDN, or an IP address.
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
||||
func DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
connCh <- info.Conn
|
||||
},
|
||||
}
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = http.Header{
|
||||
"Upgrade": []string{"ts-dial"},
|
||||
"Connection": []string{"upgrade"},
|
||||
"Dial-Host": []string{host},
|
||||
"Dial-Port": []string{fmt.Sprint(port)},
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusSwitchingProtocols {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("unexpected HTTP response: %s, %s", res.Status, body)
|
||||
}
|
||||
// From here on, the underlying net.Conn is ours to use, but there
|
||||
// is still a read buffer attached to it within resp.Body. So, we
|
||||
// must direct I/O through resp.Body, but we can still use the
|
||||
// underlying net.Conn for stuff like deadlines.
|
||||
var switchedConn net.Conn
|
||||
select {
|
||||
case switchedConn = <-connCh:
|
||||
default:
|
||||
}
|
||||
if switchedConn == nil {
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("httptrace didn't provide a connection")
|
||||
}
|
||||
rwc, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
res.Body.Close()
|
||||
return nil, errors.New("http Transport did not provide a writable body")
|
||||
}
|
||||
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -518,99 +293,3 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
}
|
||||
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?"
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@ 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 (
|
||||
@@ -60,23 +63,47 @@ 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("func Clone(dst, src interface{}) bool {")
|
||||
w(" switch src := src.(type) {")
|
||||
for _, typeName := range typeNames {
|
||||
w(" case *%s:", typeName)
|
||||
@@ -95,20 +122,7 @@ func main() {
|
||||
}
|
||||
|
||||
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 +130,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, 0644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -130,14 +149,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 +167,107 @@ 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("\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("}")
|
||||
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 +278,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
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dnsCache atomic.Value // of []byte
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
@@ -34,7 +37,6 @@ 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, ",")
|
||||
@@ -45,23 +47,23 @@ func refreshBootstrapDNS() {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsEntries[name] = addrs
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
}
|
||||
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)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -12,11 +12,9 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -25,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -34,60 +32,30 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
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 wgkey.Private
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
if *dev {
|
||||
return config{PrivateKey: key.NewNode()}
|
||||
return config{PrivateKey: mustNewKey()}
|
||||
}
|
||||
if *configPath == "" {
|
||||
if os.Getuid() == 0 {
|
||||
@@ -113,13 +81,21 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
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 {
|
||||
@@ -141,11 +117,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,9 +125,9 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
@@ -177,10 +148,7 @@ 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)
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -213,96 +181,48 @@ func main() {
|
||||
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 +232,45 @@ 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 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 +287,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 +317,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.NodePublic) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -9,25 +9,19 @@ 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"
|
||||
)
|
||||
@@ -37,52 +31,28 @@ var (
|
||||
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) {
|
||||
func (st *overallStatus) addBadf(format string, a ...interface{}) {
|
||||
st.bad = append(st.bad, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func (st *overallStatus) addGoodf(format string, a ...any) {
|
||||
func (st *overallStatus) addGoodf(format string, a ...interface{}) {
|
||||
st.good = append(st.good, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
@@ -97,65 +67,35 @@ func getOverallStatus() (o overallStatus) {
|
||||
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})
|
||||
pair := nodePair{from.Name, to.Name}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
if len(st.bad) > 0 {
|
||||
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 {
|
||||
@@ -167,71 +107,6 @@ func serve(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
@@ -242,8 +117,7 @@ func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
|
||||
}
|
||||
|
||||
type nodePair struct {
|
||||
from string // DERPNode.Name, or "UDP" for a STUN query to 'to'
|
||||
to string // DERPNode.Name
|
||||
from, to string // DERPNode.Name
|
||||
}
|
||||
|
||||
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
|
||||
@@ -303,8 +177,6 @@ func probe() error {
|
||||
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)
|
||||
@@ -317,65 +189,6 @@ func probe() error {
|
||||
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
|
||||
@@ -426,7 +239,7 @@ func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.D
|
||||
}
|
||||
|
||||
// Receive the random packet.
|
||||
recvc := make(chan any, 1) // either derp.ReceivedPacket or error
|
||||
recvc := make(chan interface{}, 1) // either derp.ReceivedPacket or error
|
||||
go func() {
|
||||
for {
|
||||
m, err := toc.Recv()
|
||||
@@ -462,7 +275,7 @@ func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.D
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
|
||||
priv := key.NewNode()
|
||||
priv := key.NewPrivate()
|
||||
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
|
||||
rid := n.RegionID
|
||||
return &tailcfg.DERPRegion{
|
||||
@@ -477,21 +290,6 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de
|
||||
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()
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
// 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.
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
@@ -72,31 +69,11 @@ func main() {
|
||||
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("", "")
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
@@ -150,9 +127,8 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
|
||||
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"
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
@@ -161,10 +137,6 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
@@ -196,7 +168,7 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
||||
MachineName: firstLabel(who.Node.ComputedName),
|
||||
MachineOS: who.Node.Hostinfo.OS(),
|
||||
MachineOS: who.Node.Hostinfo.OS,
|
||||
IP: tailscaleIP(who),
|
||||
}
|
||||
}
|
||||
@@ -206,6 +178,8 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// firstLabel s up until the first period, if any.
|
||||
func firstLabel(s string) string {
|
||||
s, _, _ = strings.Cut(s, ".")
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
173
cmd/microproxy/microproxy.go
Normal file
173
cmd/microproxy/microproxy.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// 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 := http.NewServeMux()
|
||||
tsweb.Debugger(mux) // registers /debug/*
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
|
||||
Quiet200s: true,
|
||||
Logf: 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
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"github.com/goreleaser/nfpm"
|
||||
_ "github.com/goreleaser/nfpm/deb"
|
||||
_ "github.com/goreleaser/nfpm/rpm"
|
||||
"github.com/pborman/getopt"
|
||||
)
|
||||
|
||||
// parseFiles parses a comma-separated list of colon-separated pairs
|
||||
@@ -44,21 +44,19 @@ func parseEmptyDirs(s string) []string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "output file to write")
|
||||
name := flag.String("name", "tailscale", "package name")
|
||||
description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
|
||||
goarch := flag.String("arch", "amd64", "GOARCH this package is for")
|
||||
pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
|
||||
files := flag.String("files", "", "comma-separated list of files in src:dst form")
|
||||
configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
|
||||
emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
|
||||
version := flag.String("version", "0.0.0", "version of the package")
|
||||
postinst := flag.String("postinst", "", "debian postinst script path")
|
||||
prerm := flag.String("prerm", "", "debian prerm script path")
|
||||
postrm := flag.String("postrm", "", "debian postrm script path")
|
||||
replaces := flag.String("replaces", "", "package which this package replaces, if any")
|
||||
depends := flag.String("depends", "", "comma-separated list of packages this package depends on")
|
||||
flag.Parse()
|
||||
out := getopt.StringLong("out", 'o', "", "output file to write")
|
||||
goarch := getopt.StringLong("arch", 'a', "amd64", "GOARCH this package is for")
|
||||
pkgType := getopt.StringLong("type", 't', "deb", "type of package to build (deb or rpm)")
|
||||
files := getopt.StringLong("files", 'F', "", "comma-separated list of files in src:dst form")
|
||||
configFiles := getopt.StringLong("configs", 'C', "", "like --files, but for files marked as user-editable config files")
|
||||
emptyDirs := getopt.StringLong("emptydirs", 'E', "", "comma-separated list of empty directories")
|
||||
version := getopt.StringLong("version", 0, "0.0.0", "version of the package")
|
||||
postinst := getopt.StringLong("postinst", 0, "", "debian postinst script path")
|
||||
prerm := getopt.StringLong("prerm", 0, "", "debian prerm script path")
|
||||
postrm := getopt.StringLong("postrm", 0, "", "debian postrm script path")
|
||||
replaces := getopt.StringLong("replaces", 0, "", "package which this package replaces, if any")
|
||||
depends := getopt.StringLong("depends", 0, "", "comma-separated list of packages this package depends on")
|
||||
getopt.Parse()
|
||||
|
||||
filesMap, err := parseFiles(*files)
|
||||
if err != nil {
|
||||
@@ -70,12 +68,12 @@ func main() {
|
||||
}
|
||||
emptyDirList := parseEmptyDirs(*emptyDirs)
|
||||
info := nfpm.WithDefaults(&nfpm.Info{
|
||||
Name: *name,
|
||||
Name: "tailscale",
|
||||
Arch: *goarch,
|
||||
Platform: "linux",
|
||||
Version: *version,
|
||||
Maintainer: "Tailscale Inc <info@tailscale.com>",
|
||||
Description: *description,
|
||||
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO",
|
||||
Homepage: "https://www.tailscale.com",
|
||||
License: "MIT",
|
||||
Overridables: nfpm.Overridables{
|
||||
|
||||
4
cmd/nginx-auth/.gitignore
vendored
4
cmd/nginx-auth/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
nga.sock
|
||||
*.deb
|
||||
*.rpm
|
||||
tailscale.nginx-auth
|
||||
@@ -1,157 +0,0 @@
|
||||
# nginx-auth
|
||||
|
||||
This is a tool that allows users to use Tailscale Whois authentication with
|
||||
NGINX as a reverse proxy. This allows users that already have a bunch of
|
||||
services hosted on an internal NGINX server to point those domains to the
|
||||
Tailscale IP of the NGINX server and then seamlessly use Tailscale for
|
||||
authentication.
|
||||
|
||||
Many thanks to [@zrail](https://twitter.com/zrail/status/1511788463586222087) on
|
||||
Twitter for introducing the basic idea and offering some sample code. This
|
||||
program is based on that sample code with security enhancements. Namely:
|
||||
|
||||
* This listens over a UNIX socket instead of a TCP socket, to prevent
|
||||
leakage to the network
|
||||
* This uses systemd socket activation so that systemd owns the socket
|
||||
and can then lock down the service to the bare minimum required to do
|
||||
its job without having to worry about dropping permissions
|
||||
* This provides additional information in HTTP response headers that can
|
||||
be useful for integrating with various services
|
||||
|
||||
## Configuration
|
||||
|
||||
In order to protect a service with this tool, do the following in the respective
|
||||
`server` block:
|
||||
|
||||
Create an authentication location with the `internal` flag set:
|
||||
|
||||
```nginx
|
||||
location /auth {
|
||||
internal;
|
||||
|
||||
proxy_pass http://unix:/run/tailscale.nginx-auth.sock;
|
||||
proxy_pass_request_body off;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Remote-Addr $remote_addr;
|
||||
proxy_set_header Remote-Port $remote_port;
|
||||
proxy_set_header Original-URI $request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
Then add the following to the `location /` block:
|
||||
|
||||
```
|
||||
auth_request /auth;
|
||||
auth_request_set $auth_user $upstream_http_tailscale_user;
|
||||
auth_request_set $auth_name $upstream_http_tailscale_name;
|
||||
auth_request_set $auth_login $upstream_http_tailscale_login;
|
||||
auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
|
||||
auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
|
||||
|
||||
proxy_set_header X-Webauth-User "$auth_user";
|
||||
proxy_set_header X-Webauth-Name "$auth_name";
|
||||
proxy_set_header X-Webauth-Login "$auth_login";
|
||||
proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
|
||||
proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
|
||||
```
|
||||
|
||||
When this configuration is used with a Go HTTP handler such as this:
|
||||
|
||||
```go
|
||||
http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", " ")
|
||||
e.Encode(r.Header)
|
||||
})
|
||||
```
|
||||
|
||||
You will get output like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"Accept": [
|
||||
"*/*"
|
||||
],
|
||||
"Connection": [
|
||||
"upgrade"
|
||||
],
|
||||
"User-Agent": [
|
||||
"curl/7.82.0"
|
||||
],
|
||||
"X-Webauth-Login": [
|
||||
"Xe"
|
||||
],
|
||||
"X-Webauth-Name": [
|
||||
"Xe Iaso"
|
||||
],
|
||||
"X-Webauth-Profile-Picture": [
|
||||
"https://avatars.githubusercontent.com/u/529003?v=4"
|
||||
],
|
||||
"X-Webauth-Tailnet": [
|
||||
"cetacean.org.github"
|
||||
]
|
||||
"X-Webauth-User": [
|
||||
"Xe@github"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Headers
|
||||
|
||||
The authentication service provides the following headers to decorate your
|
||||
proxied requests:
|
||||
|
||||
| Header | Example Value | Description |
|
||||
| :------ | :-------------- | :---------- |
|
||||
| `Tailscale-User` | `azurediamond@hunter2.net` | The Tailscale username the remote machine is logged in as in user@host form |
|
||||
| `Tailscale-Login` | `azurediamond` | The user portion of the Tailscale username the remote machine is logged in as |
|
||||
| `Tailscale-Name` | `Azure Diamond` | The "real name" of the Tailscale user the machine is logged in as |
|
||||
| `Tailscale-Profile-Picture` | `https://i.kym-cdn.com/photos/images/newsfeed/001/065/963/ae0.png` | The profile picture provided by the Identity Provider your tailnet uses |
|
||||
| `Tailscale-Tailnet` | `hunter2.net` | The tailnet name |
|
||||
|
||||
Most of the time you can set `X-Webauth-User` to the contents of the
|
||||
`Tailscale-User` header, but some services may not accept a username with an `@`
|
||||
symbol in it. If this is the case, set `X-Webauth-User` to the `Tailscale-Login`
|
||||
header.
|
||||
|
||||
The `Tailscale-Tailnet` header can help you identify which tailnet the session
|
||||
is coming from. If you are using node sharing, this can help you make sure that
|
||||
you aren't giving administrative access to people outside your tailnet.
|
||||
|
||||
### Allow Requests From Only One Tailnet
|
||||
|
||||
If you want to prevent node sharing from allowing users to access a service, add
|
||||
the `Expected-Tailnet` header to your auth request:
|
||||
|
||||
```nginx
|
||||
location /auth {
|
||||
# ...
|
||||
proxy_set_header Expected-Tailnet "tailscale.com";
|
||||
}
|
||||
```
|
||||
|
||||
If a user from a different tailnet tries to use that service, this will return a
|
||||
generic "forbidden" error page:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head><title>403 Forbidden</title></head>
|
||||
<body>
|
||||
<center><h1>403 Forbidden</h1></center>
|
||||
<hr><center>nginx/1.18.0 (Ubuntu)</center>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Install `cmd/mkpkg`:
|
||||
|
||||
```
|
||||
cd .. && go install ./mkpkg
|
||||
```
|
||||
|
||||
Then run `./mkdeb.sh`. It will emit a `.deb` and `.rpm` package for amd64
|
||||
machines (Linux uname flag: `x86_64`). You can add these to your deployment
|
||||
methods as you see fit.
|
||||
@@ -1,14 +0,0 @@
|
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
|
||||
deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
else
|
||||
deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
|
||||
if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ -d /run/systemd/system ] ; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
||||
if [ "$1" = "remove" ]; then
|
||||
deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$1" = "purge" ]; then
|
||||
deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "remove" ]; then
|
||||
if [ -d /run/systemd/system ]; then
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
|
||||
|
||||
VERSION=0.1.1
|
||||
|
||||
mkpkg \
|
||||
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
|
||||
--name=tailscale-nginx-auth \
|
||||
--version=${VERSION} \
|
||||
--type=deb \
|
||||
--arch=amd64 \
|
||||
--postinst=deb/postinst.sh \
|
||||
--postrm=deb/postrm.sh \
|
||||
--prerm=deb/prerm.sh \
|
||||
--description="Tailscale NGINX authentication protocol handler" \
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
|
||||
|
||||
mkpkg \
|
||||
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
|
||||
--name=tailscale-nginx-auth \
|
||||
--version=${VERSION} \
|
||||
--type=rpm \
|
||||
--arch=amd64 \
|
||||
--postinst=rpm/postinst.sh \
|
||||
--postrm=rpm/postrm.sh \
|
||||
--prerm=rpm/prerm.sh \
|
||||
--description="Tailscale NGINX authentication protocol handler" \
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
|
||||
@@ -1,127 +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 linux
|
||||
|
||||
// Command nginx-auth is a tool that allows users to use Tailscale Whois
|
||||
// authentication with NGINX as a reverse proxy. This allows users that
|
||||
// already have a bunch of services hosted on an internal NGINX server
|
||||
// to point those domains to the Tailscale IP of the NGINX server and
|
||||
// then seamlessly use Tailscale for authentication.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-systemd/activation"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var (
|
||||
sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
remoteHost := r.Header.Get("Remote-Addr")
|
||||
remotePort := r.Header.Get("Remote-Port")
|
||||
if remoteHost == "" || remotePort == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
|
||||
return
|
||||
}
|
||||
|
||||
remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
|
||||
remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("remote address and port are not valid: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't look up %s: %v", remoteAddr, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(info.Node.Tags) != 0 {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
|
||||
return
|
||||
}
|
||||
|
||||
_, tailnet, ok := strings.Cut(info.Node.Name, info.Node.ComputedName+".")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
tailnet, _, ok = strings.Cut(tailnet, ".beta.tailscale.net")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
|
||||
h.Set("Tailscale-User", info.UserProfile.LoginName)
|
||||
h.Set("Tailscale-Name", info.UserProfile.DisplayName)
|
||||
h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
|
||||
h.Set("Tailscale-Tailnet", tailnet)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
if *sockPath != "" {
|
||||
_ = os.Remove(*sockPath) // ignore error, this file may not already exist
|
||||
ln, err := net.Listen("unix", *sockPath)
|
||||
if err != nil {
|
||||
log.Fatalf("can't listen on %s: %v", *sockPath, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
log.Printf("listening on %s", *sockPath)
|
||||
log.Fatal(http.Serve(ln, mux))
|
||||
}
|
||||
|
||||
listeners, err := activation.Listeners()
|
||||
if err != nil {
|
||||
log.Fatalf("no sockets passed to this service with systemd: %v", err)
|
||||
}
|
||||
|
||||
// NOTE(Xe): normally you'd want to make a waitgroup here and then register
|
||||
// each listener with it. In this case I want this to blow up horribly if
|
||||
// any of the listeners stop working. systemd will restart it due to the
|
||||
// socket activation at play.
|
||||
//
|
||||
// TL;DR: Let it crash, it will come back
|
||||
for _, ln := range listeners {
|
||||
go func(ln net.Listener) {
|
||||
log.Printf("listening on %s", ln.Addr())
|
||||
log.Fatal(http.Serve(ln, mux))
|
||||
}(ln)
|
||||
}
|
||||
|
||||
for {
|
||||
select {}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# $1 == 0 for uninstallation.
|
||||
# $1 == 1 for removing old package during upgrade.
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || :
|
||||
if [ $1 -ge 1 ] ; then
|
||||
# Package upgrade, not uninstall
|
||||
systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
|
||||
systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
# $1 == 0 for uninstallation.
|
||||
# $1 == 1 for removing old package during upgrade.
|
||||
|
||||
if [ $1 -eq 0 ] ; then
|
||||
# Package removal, not upgrade
|
||||
systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
|
||||
systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
|
||||
systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
|
||||
fi
|
||||
@@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=Tailscale NGINX Authentication service
|
||||
After=nginx.service
|
||||
Wants=nginx.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/sbin/tailscale.nginx-auth
|
||||
DynamicUser=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=Tailscale NGINX Authentication socket
|
||||
PartOf=tailscale.nginx-auth.service
|
||||
|
||||
[Socket]
|
||||
ListenStream=/var/run/tailscale.nginx-auth.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +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.
|
||||
|
||||
// proxy-to-grafana is a reverse proxy which identifies users based on their
|
||||
// originating Tailscale identity and maps them to corresponding Grafana
|
||||
// users, creating them if needed.
|
||||
//
|
||||
// It uses Grafana's AuthProxy feature:
|
||||
// https://grafana.com/docs/grafana/latest/auth/auth-proxy/
|
||||
//
|
||||
// Set the TS_AUTHKEY environment variable to have this server automatically
|
||||
// join your tailnet, or look for the logged auth link on first start.
|
||||
//
|
||||
// Use this Grafana configuration to enable the auth proxy:
|
||||
//
|
||||
// [auth.proxy]
|
||||
// enabled = true
|
||||
// header_name = X-WEBAUTH-USER
|
||||
// header_property = username
|
||||
// auto_sign_up = true
|
||||
// whitelist = 127.0.0.1
|
||||
// headers = Name:X-WEBAUTH-NAME
|
||||
// enable_login_token = true
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
var (
|
||||
hostname = flag.String("hostname", "", "Tailscale hostname to serve on, used as the base name for MagicDNS or subdomain in your domain alias for HTTPS.")
|
||||
backendAddr = flag.String("backend-addr", "", "Address of the Grafana server served over HTTP, in host:port format. Typically localhost:nnnn.")
|
||||
tailscaleDir = flag.String("state-dir", "./", "Alternate directory to use for Tailscale state storage. If empty, a default is used.")
|
||||
useHTTPS = flag.Bool("use-https", false, "Serve over HTTPS via your *.ts.net subdomain if enabled in Tailscale admin.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *hostname == "" || strings.Contains(*hostname, ".") {
|
||||
log.Fatal("missing or invalid --hostname")
|
||||
}
|
||||
if *backendAddr == "" {
|
||||
log.Fatal("missing --backend-addr")
|
||||
}
|
||||
ts := &tsnet.Server{
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("http://%s", *backendAddr))
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't parse backend address: %v", err)
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
modifyRequest(req)
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
if *useHTTPS {
|
||||
ln, err = ts.Listen("tcp", ":443")
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
})
|
||||
|
||||
go func() {
|
||||
// wait for tailscale to start before trying to fetch cert names
|
||||
for i := 0; i < 60; i++ {
|
||||
st, err := tailscale.Status(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("error retrieving tailscale status; retrying: %v", err)
|
||||
} else {
|
||||
log.Printf("tailscale status: %v", st.BackendState)
|
||||
if st.BackendState == "Running" {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
l80, err := ts.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
name, ok := tailscale.ExpandSNIName(context.Background(), *hostname)
|
||||
if !ok {
|
||||
log.Fatalf("can't get hostname for https redirect")
|
||||
}
|
||||
if err := http.Serve(l80, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, fmt.Sprintf("https://%s", name), http.StatusMovedPermanently)
|
||||
})); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
ln, err = ts.Listen("tcp", ":80")
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("proxy-to-grafana running at %v, proxying to %v", ln.Addr(), *backendAddr)
|
||||
log.Fatal(http.Serve(ln, proxy))
|
||||
}
|
||||
|
||||
func modifyRequest(req *http.Request) {
|
||||
// with enable_login_token set to true, we get a cookie that handles
|
||||
// auth for paths that are not /login
|
||||
if req.URL.Path != "/login" {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := getTailscaleUser(req.Context(), req.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Printf("error getting Tailscale user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-Webauth-User", user.LoginName)
|
||||
req.Header.Set("X-Webauth-Name", user.DisplayName)
|
||||
}
|
||||
|
||||
func getTailscaleUser(ctx context.Context, ipPort string) (*tailcfg.UserProfile, error) {
|
||||
whois, err := tailscale.WhoIs(ctx, ipPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
||||
}
|
||||
if len(whois.Node.Tags) != 0 {
|
||||
return nil, fmt.Errorf("tagged nodes are not users")
|
||||
}
|
||||
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
||||
return nil, fmt.Errorf("failed to identify remote user")
|
||||
}
|
||||
|
||||
return whois.UserProfile, nil
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/net/speedtest"
|
||||
)
|
||||
|
||||
|
||||
155
cmd/tailscale/cli/admin.go
Normal file
155
cmd/tailscale/cli/admin.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dsnet/golib/jsonfmt"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
)
|
||||
|
||||
const tailscaleAPIURL = "https://api.tailscale.com/api"
|
||||
|
||||
var adminCmd = &ffcli.Command{
|
||||
Name: "admin",
|
||||
ShortUsage: "admin <subcommand> [command flags]",
|
||||
ShortHelp: "Administrate a tailnet",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The "tailscale admin" command administrates a tailnet through the CLI.
|
||||
It is a wrapper over the RESTful API served at ` + tailscaleAPIURL + `.
|
||||
See https://github.com/tailscale/tailscale/blob/main/api.md for more information
|
||||
about the API itself.
|
||||
|
||||
In order for the "admin" command to call the API, it needs an API key,
|
||||
which is specified by setting the TAILSCALE_API_KEY environment variable.
|
||||
Also, to easy usage, the tailnet to administrate can be specified through the
|
||||
TAILSCALE_NET_NAME environment variable, or specified with the -tailnet flag.
|
||||
|
||||
Visit https://login.tailscale.com/admin/settings/authkeys in order to obtain
|
||||
an API key.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||
// TODO(dsnet): Can we determine the default tailnet from what this
|
||||
// device is currently part of? Alternatively, when add specific logic
|
||||
// to handle auth keys, we can always associate a given key with a
|
||||
// specific tailnet.
|
||||
fs.StringVar(&adminArgs.tailnet, "tailnet", os.Getenv("TAILSCALE_NET_NAME"), "which tailnet to administrate")
|
||||
return fs
|
||||
})(),
|
||||
// TODO(dsnet): Handle users, groups, dns.
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "acl",
|
||||
ShortUsage: "acl <subcommand> [command flags]",
|
||||
ShortHelp: "Manage the ACL for a tailnet",
|
||||
// TODO(dsnet): Handle preview.
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "get",
|
||||
ShortUsage: "get",
|
||||
ShortHelp: "Downloads the HuJSON ACL file to stdout",
|
||||
Exec: checkAdminKey(runAdminACLGet),
|
||||
}, {
|
||||
Name: "set",
|
||||
ShortUsage: "set",
|
||||
ShortHelp: "Uploads the HuJSON ACL file from stdin",
|
||||
Exec: checkAdminKey(runAdminACLSet),
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}, {
|
||||
Name: "devices",
|
||||
ShortUsage: "devices <subcommand> [command flags]",
|
||||
ShortHelp: "Manage devices in a tailnet",
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "list",
|
||||
ShortUsage: "list",
|
||||
ShortHelp: "List all devices in a tailnet",
|
||||
Exec: checkAdminKey(runAdminDevicesList),
|
||||
}, {
|
||||
Name: "get",
|
||||
ShortUsage: "get <id>",
|
||||
ShortHelp: "Get information about a specific device",
|
||||
Exec: checkAdminKey(runAdminDevicesGet),
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}
|
||||
|
||||
var adminArgs struct {
|
||||
tailnet string // which tailnet to operate upon
|
||||
}
|
||||
|
||||
func checkAdminKey(f func(context.Context, string, []string) error) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
// TODO(dsnet): We should have a subcommand or flag to manage keys.
|
||||
// Use of an environment variable is a temporary hack.
|
||||
key := os.Getenv("TAILSCALE_API_KEY")
|
||||
if !strings.HasPrefix(key, "tskey-") {
|
||||
return errors.New("no API key specified")
|
||||
}
|
||||
return f(ctx, key, args)
|
||||
}
|
||||
}
|
||||
|
||||
func runAdminACLGet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/acl", nil, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminACLSet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodPost, "/v2/tailnet/"+adminArgs.tailnet+"/acl", os.Stdin, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminDevicesList(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/devices", nil, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminDevicesGet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/device/"+args[0], nil, os.Stdout)
|
||||
}
|
||||
|
||||
func adminCallAPI(ctx context.Context, key, method, path string, in io.Reader, out io.Writer) error {
|
||||
req, err := http.NewRequestWithContext(ctx, method, tailscaleAPIURL+path, in)
|
||||
req.SetBasicAuth(key, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send HTTP request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to receive HTTP response: %w", err)
|
||||
}
|
||||
b, err = jsonfmt.Format(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to format JSON response: %w", err)
|
||||
}
|
||||
_, err = out.Write(b)
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -7,8 +7,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
@@ -32,6 +33,6 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outln(logMarker)
|
||||
fmt.Println(logMarker)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -19,36 +19,17 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"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.
|
||||
@@ -95,50 +76,17 @@ func ActLikeCLI() bool {
|
||||
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
|
||||
func runHelp(context.Context, []string) error {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
// 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"}
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -155,73 +103,47 @@ change in the future.
|
||||
upCmd,
|
||||
downCmd,
|
||||
logoutCmd,
|
||||
adminCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
sshCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
Exec: runHelp,
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
if envknob.UseWIPCode() {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -229,8 +151,7 @@ var rootArgs struct {
|
||||
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")
|
||||
|
||||
@@ -13,15 +13,12 @@ import (
|
||||
"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.
|
||||
@@ -59,7 +56,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
distro distro.Distro
|
||||
|
||||
want string
|
||||
}{
|
||||
@@ -316,7 +312,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
@@ -333,7 +328,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "openbsd",
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
@@ -380,7 +375,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: accidentalUpPrefix + " --auth-key=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
want: accidentalUpPrefix + " --authkey=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_ip_pref",
|
||||
@@ -409,21 +404,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeAllowLANAccess: true,
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
|
||||
},
|
||||
{
|
||||
name: "ignore_login_server_synonym",
|
||||
flags: []string{"--login-server=https://controlplane.tailscale.com"},
|
||||
@@ -446,38 +426,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
||||
},
|
||||
{
|
||||
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
||||
// migth've had old an install, and we don't support --accept-routes anyway.
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: distro.Synology,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Same test case as "synology_permit_omit_accept_routes" above, but
|
||||
// on non-Synology distro.
|
||||
name: "not_synology_dont_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: "", // not Synology
|
||||
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -487,8 +435,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
}
|
||||
var upArgs upArgsT
|
||||
flagSet := newUpFlagSet(goos, &upArgs)
|
||||
flags := CleanUpArgs(tt.flags)
|
||||
flagSet.Parse(flags)
|
||||
flagSet.Parse(tt.flags)
|
||||
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -499,7 +446,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
@@ -548,7 +494,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
@@ -586,7 +531,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
@@ -631,7 +576,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
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?`,
|
||||
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_nodivert",
|
||||
@@ -659,43 +604,13 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "via_route_good",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a::bb:10.0.0.0/112",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "via_route_short_prefix",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a::/64",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantErr: "fd7a:115c:a1e0:b1a::/64 4-in-6 prefix must be at least a /96",
|
||||
},
|
||||
{
|
||||
name: "via_route_short_reserved_siteid",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xff or less",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf tstest.MemLogger
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
@@ -704,7 +619,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnBuf.Logf, st, goos)
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
@@ -820,33 +735,6 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
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{},
|
||||
@@ -873,18 +761,6 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
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) {
|
||||
@@ -892,8 +768,7 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
tt.env.goos = "linux"
|
||||
}
|
||||
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
|
||||
flags := CleanUpArgs(tt.flags)
|
||||
tt.env.flagSet.Parse(flags)
|
||||
tt.env.flagSet.Parse(tt.flags)
|
||||
|
||||
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
|
||||
if err != nil {
|
||||
@@ -912,45 +787,12 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
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))
|
||||
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -5,10 +5,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
@@ -17,168 +14,115 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
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 := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
|
||||
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
|
||||
fs.BoolVar(&debugArgs.derpMap, "derp", false, "If true, dump DERP map")
|
||||
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
|
||||
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
|
||||
return fs
|
||||
})(),
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "derp-map",
|
||||
Exec: runDERPMap,
|
||||
ShortHelp: "print DERP map",
|
||||
},
|
||||
{
|
||||
Name: "daemon-goroutines",
|
||||
Exec: runDaemonGoroutines,
|
||||
ShortHelp: "print tailscaled's goroutines",
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Exec: runDaemonMetrics,
|
||||
ShortHelp: "print tailscaled's metrics",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("metrics")
|
||||
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "env",
|
||||
Exec: runEnv,
|
||||
ShortHelp: "print cmd/tailscale environment",
|
||||
},
|
||||
{
|
||||
Name: "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
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "via",
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
localCreds bool
|
||||
goroutines bool
|
||||
ipn bool
|
||||
netMap bool
|
||||
derpMap bool
|
||||
file string
|
||||
prefs bool
|
||||
pretty bool
|
||||
}
|
||||
|
||||
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.localCreds {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
if debugArgs.prefs {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if 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.pretty {
|
||||
fmt.Println(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
fmt.Println(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.derpMap {
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.ipn {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !debugArgs.netMap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
if debugArgs.file != "" {
|
||||
usedFlag = true // TODO(bradfitz): add "file" subcommand
|
||||
if debugArgs.file == "get" {
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
fatalf("%v\n", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(Stdout)
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
@@ -192,210 +136,8 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
log.Printf("Size: %v\n", size)
|
||||
io.Copy(Stdout, rc)
|
||||
io.Copy(os.Stdout, rc)
|
||||
return nil
|
||||
}
|
||||
if usedFlag {
|
||||
// TODO(bradfitz): delete this path when all debug flags are migrated
|
||||
// to subcommands.
|
||||
return nil
|
||||
}
|
||||
return errors.New("see 'tailscale debug --help")
|
||||
}
|
||||
|
||||
func runLocalCreds(ctx context.Context, args []string) error {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
|
||||
return nil
|
||||
}
|
||||
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
|
||||
var prefsArgs struct {
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func runPrefs(ctx context.Context, args []string) error {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prefsArgs.pretty {
|
||||
outln(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
outln(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var watchIPNArgs struct {
|
||||
netmap bool
|
||||
}
|
||||
|
||||
func runWatchIPN(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !watchIPNArgs.netmap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
|
||||
func runDERPMap(ctx context.Context, args []string) error {
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
||||
|
||||
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 runVia(ctx context.Context, args []string) error {
|
||||
switch len(args) {
|
||||
default:
|
||||
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
|
||||
case 1:
|
||||
ipp, err := netaddr.ParseIPPrefix(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ipp.IP().Is6() {
|
||||
return errors.New("with one argument, expect an IPv6 CIDR")
|
||||
}
|
||||
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
|
||||
return errors.New("not a via route")
|
||||
}
|
||||
if ipp.Bits() < 96 {
|
||||
return errors.New("short length, want /96 or more")
|
||||
}
|
||||
v4 := tsaddr.UnmapVia(ipp.IP())
|
||||
a := ipp.IP().As16()
|
||||
siteID := binary.BigEndian.Uint32(a[8:12])
|
||||
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
|
||||
case 2:
|
||||
siteID, err := strconv.ParseUint(args[0], 0, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
|
||||
}
|
||||
if siteID > 0xff {
|
||||
return fmt.Errorf("site-id values over 255 are currently reserved")
|
||||
}
|
||||
ipp, err := netaddr.ParseIPPrefix(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
via, err := tsaddr.MapVia(uint32(siteID), ipp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,23 +25,23 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
|
||||
}
|
||||
var foundProc ps.Process
|
||||
found := false
|
||||
for _, proc := range procs {
|
||||
base := filepath.Base(proc.Executable())
|
||||
if base == "tailscaled" {
|
||||
foundProc = proc
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "darwin" && base == "IPNExtension" {
|
||||
foundProc = proc
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
|
||||
foundProc = proc
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundProc == nil {
|
||||
if !found {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
|
||||
@@ -52,5 +52,5 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
@@ -23,7 +25,7 @@ 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)
|
||||
@@ -31,7 +33,7 @@ func runDown(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
}
|
||||
if st.BackendState == "Stopped" {
|
||||
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
|
||||
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
|
||||
return nil
|
||||
}
|
||||
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
|
||||
@@ -11,25 +11,25 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/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"
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ var fileCpCmd = &ffcli.Command{
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("cp")
|
||||
fs := flag.NewFlagSet("cp", flag.ExitOnError)
|
||||
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")
|
||||
@@ -91,17 +91,17 @@ func runCp(ctx context.Context, args []string) error {
|
||||
} else if hadBrackets && (err != nil || !ip.Is6()) {
|
||||
return errors.New("unexpected brackets around target")
|
||||
}
|
||||
ip, _, err := tailscaleIPFromArg(ctx, target)
|
||||
ip, err := tailscaleIPFromArg(ctx, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stableID, isOffline, err := getTargetStableID(ctx, ip)
|
||||
peerAPIBase, isOffline, err := discoverPeerAPIBase(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)
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
@@ -149,26 +149,37 @@ func runCp(ctx context.Context, args []string) error {
|
||||
name = filepath.Base(fileArg)
|
||||
}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
|
||||
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
|
||||
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)
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
@@ -184,7 +195,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return n.StableID, isOffline, nil
|
||||
return ft.PeerAPIURL, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
@@ -282,176 +293,27 @@ func runCpTargets(ctx context.Context, args []string) error {
|
||||
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)
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var fileGetCmd = &ffcli.Command{
|
||||
Name: "get",
|
||||
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
|
||||
ShortUsage: "file get [--wait] [--verbose] <target-directory>",
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("get")
|
||||
fs := flag.NewFlagSet("get", flag.ExitOnError)
|
||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
|
||||
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
|
||||
loop 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)
|
||||
}
|
||||
defer rc.Close()
|
||||
f, err := openFileOrSubstitute(dir, wf.Name, getArgs.conflict)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return "", 0, fmt.Errorf("failed to write %v: %v", f.Name(), err)
|
||||
}
|
||||
return f.Name(), size, f.Close()
|
||||
}
|
||||
|
||||
func runFileGetOneBatch(ctx context.Context, dir string) []error {
|
||||
var wfs []apitype.WaitingFile
|
||||
var err error
|
||||
var errs []error
|
||||
for len(errs) == 0 {
|
||||
wfs, err = tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting WaitingFiles: %w", err))
|
||||
break
|
||||
}
|
||||
if len(wfs) != 0 || !(getArgs.wait || getArgs.loop) {
|
||||
break
|
||||
}
|
||||
if getArgs.verbose {
|
||||
printf("waiting for file...")
|
||||
}
|
||||
if err := waitForFile(ctx); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
for i, wf := range wfs {
|
||||
if len(errs) > 100 {
|
||||
// Likely, everything is broken.
|
||||
// Don't try to receive any more files in this batch.
|
||||
errs = append(errs, fmt.Errorf("too many errors in runFileGetOneBatch(). %d files unexamined", len(wfs) - i))
|
||||
break
|
||||
}
|
||||
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 deleted == 0 && len(wfs) > 0 {
|
||||
// persistently stuck files are basically an error
|
||||
errs = append(errs, fmt.Errorf("moved %d/%d files", deleted, len(wfs)))
|
||||
} else if getArgs.verbose {
|
||||
printf("moved %d/%d files\n", deleted, len(wfs))
|
||||
}
|
||||
return errs
|
||||
var getArgs struct {
|
||||
wait bool
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runFileGet(ctx context.Context, args []string) error {
|
||||
@@ -468,35 +330,59 @@ func runFileGet(ctx context.Context, args []string) error {
|
||||
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", dir)
|
||||
}
|
||||
if getArgs.loop {
|
||||
for {
|
||||
errs := runFileGetOneBatch(ctx, dir)
|
||||
for _, err := range errs {
|
||||
outln(err)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
// It's possible whatever caused the error(s) (e.g. conflicting target file,
|
||||
// full disk, unwritable target directory) will re-occur if we try again so
|
||||
// let's back off and not busy loop on error.
|
||||
//
|
||||
// If we've been invoked as:
|
||||
// tailscale file get --conflict=skip ~/Downloads
|
||||
// then any file coming in named the same as one in ~/Downloads will always
|
||||
// appear as an "error" until the user clears it, but other incoming files
|
||||
// should be receivable when they arrive, so let's not wait too long to
|
||||
// check again.
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
var wfs []apitype.WaitingFile
|
||||
var err error
|
||||
for {
|
||||
wfs, err = tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
if len(wfs) != 0 || !getArgs.wait {
|
||||
break
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("waiting for file...")
|
||||
}
|
||||
if err := waitForFile(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
errs := runFileGetOneBatch(ctx, dir)
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening inbox file %q: %v", wf.Name, err)
|
||||
}
|
||||
targetFile := filepath.Join(dir, wf.Name)
|
||||
of, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(targetFile); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite %v", targetFile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(of, rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write %v: %v", targetFile, err)
|
||||
}
|
||||
if err := of.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("wrote %v (%d bytes)", wf.Name, size)
|
||||
}
|
||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||
return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
for _, err := range errs[:len(errs)-1] {
|
||||
outln(err)
|
||||
if getArgs.verbose {
|
||||
log.Printf("moved %d files", deleted)
|
||||
}
|
||||
return errs[len(errs)-1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func wipeInbox(ctx context.Context) error {
|
||||
@@ -505,7 +391,7 @@ func wipeInbox(ctx context.Context) error {
|
||||
}
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %w", err)
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
@@ -527,10 +413,9 @@ func waitForFile(ctx context.Context) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
fileWaiting := make(chan bool, 1)
|
||||
notifyError := make(chan error, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
notifyError <- fmt.Errorf("Notify.ErrMessage: %v", *n.ErrMessage)
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
@@ -547,7 +432,5 @@ func waitForFile(ctx context.Context) error {
|
||||
return pumpCtx.Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-notifyError:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var idTokenCmd = &ffcli.Command{
|
||||
Name: "id-token",
|
||||
ShortUsage: "id-token <aud>",
|
||||
ShortHelp: "fetch an OIDC id-token for the Tailscale machine",
|
||||
Exec: runIDToken,
|
||||
}
|
||||
|
||||
func runIDToken(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: id-token <aud>")
|
||||
}
|
||||
|
||||
tr, err := tailscale.IDToken(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(tr.IDToken)
|
||||
return nil
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -18,13 +18,12 @@ import (
|
||||
|
||||
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.",
|
||||
ShortUsage: "ip [-4] [-6] [peername]",
|
||||
ShortHelp: "Show current Tailscale IP address(es)",
|
||||
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
|
||||
Exec: runIP,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ip")
|
||||
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
|
||||
fs := flag.NewFlagSet("ip", flag.ExitOnError)
|
||||
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
|
||||
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
|
||||
return fs
|
||||
@@ -32,14 +31,13 @@ var ipCmd = &ffcli.Command{
|
||||
}
|
||||
|
||||
var ipArgs struct {
|
||||
want1 bool
|
||||
want4 bool
|
||||
want6 bool
|
||||
}
|
||||
|
||||
func runIP(ctx context.Context, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return errors.New("too many arguments, expected at most one peer")
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
var of string
|
||||
if len(args) == 1 {
|
||||
@@ -47,14 +45,8 @@ func runIP(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
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 {
|
||||
return errors.New("tailscale up -4 and -6 are mutually exclusive")
|
||||
}
|
||||
if !v4 && !v6 {
|
||||
v4, v6 = true, true
|
||||
@@ -65,7 +57,7 @@ func runIP(ctx context.Context, args []string) error {
|
||||
}
|
||||
ips := st.TailscaleIPs
|
||||
if of != "" {
|
||||
ip, _, err := tailscaleIPFromArg(ctx, of)
|
||||
ip, err := tailscaleIPFromArg(ctx, of)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,14 +71,11 @@ func runIP(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
|
||||
}
|
||||
|
||||
if ipArgs.want1 {
|
||||
ips = ips[:1]
|
||||
}
|
||||
match := false
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
match = true
|
||||
outln(ip)
|
||||
fmt.Println(ip)
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
@@ -112,12 +101,5 @@ func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus,
|
||||
}
|
||||
}
|
||||
}
|
||||
if ps := st.Self; ps != nil {
|
||||
for _, pip := range ps.TailscaleIPs {
|
||||
if ip == pip {
|
||||
return ps, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ a reauthentication.
|
||||
|
||||
func runLogout(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)
|
||||
}
|
||||
return tailscale.Logout(ctx)
|
||||
}
|
||||
|
||||
@@ -1,63 +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"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var ncCmd = &ffcli.Command{
|
||||
Name: "nc",
|
||||
ShortUsage: "nc <hostname-or-IP> <port>",
|
||||
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
|
||||
Exec: runNC,
|
||||
}
|
||||
|
||||
func runNC(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)
|
||||
}
|
||||
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: nc <hostname-or-IP> <port>")
|
||||
}
|
||||
|
||||
hostOrIP, portStr := args[0], args[1]
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port number %q", portStr)
|
||||
}
|
||||
|
||||
// TODO(bradfitz): also add UDP too, via flag?
|
||||
c, err := tailscale.DialTCP(ctx, hostOrIP, uint16(port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Dial(%q, %v): %w", hostOrIP, port, err)
|
||||
}
|
||||
defer c.Close()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(os.Stdout, c)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(c, os.Stdin)
|
||||
errc <- err
|
||||
}()
|
||||
return <-errc
|
||||
}
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
@@ -33,7 +33,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,7 +49,7 @@ var netcheckArgs struct {
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
@@ -60,15 +60,11 @@ 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 {
|
||||
if err != nil {
|
||||
dm, err = prodDERPMap(ctx, http.DefaultClient)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -82,7 +78,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 +108,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 +164,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
|
||||
|
||||
@@ -11,11 +11,10 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -46,7 +45,7 @@ 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)")
|
||||
@@ -65,16 +64,6 @@ var pingArgs struct {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -85,7 +74,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fatalf("Notify.ErrMessage: %v", *n.ErrMessage)
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
@@ -95,14 +84,10 @@ func runPing(ctx context.Context, args []string) error {
|
||||
go func() { pumpErr <- pump(ctx, bc, c) }()
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self {
|
||||
printf("%v is local Tailscale IP\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
@@ -116,16 +101,12 @@ func runPing(ctx context.Context, args []string) error {
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
printf("timeout waiting for ping reply\n")
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
case pr := <-prc:
|
||||
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)
|
||||
@@ -143,7 +124,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
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)
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
@@ -166,39 +147,33 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, self bool, err error) {
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
return hostOrIP, false, nil
|
||||
return hostOrIP, 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
|
||||
return "", err
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if match(ps) {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
if len(ps.TailscaleIPs) == 0 {
|
||||
return "", false, errors.New("node found but lacks an IP")
|
||||
return "", errors.New("node found but lacks an IP")
|
||||
}
|
||||
return ps.TailscaleIPs[0].String(), false, nil
|
||||
return ps.TailscaleIPs[0].String(), 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)
|
||||
return "", 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)
|
||||
return "", fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
return addrs[0], false, nil
|
||||
return addrs[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var sshCmd = &ffcli.Command{
|
||||
Name: "ssh",
|
||||
ShortUsage: "ssh [user@]<host> [args...]",
|
||||
ShortHelp: "SSH to a Tailscale machine",
|
||||
Exec: runSSH,
|
||||
}
|
||||
|
||||
func runSSH(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: ssh [user@]<host>")
|
||||
}
|
||||
arg, argRest := args[0], args[1:]
|
||||
username, host, ok := strings.Cut(arg, "@")
|
||||
if !ok {
|
||||
host = arg
|
||||
lu, err := user.Current()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
username = lu.Username
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// hostForSSH is the hostname we'll tell OpenSSH we're
|
||||
// connecting to, so we have to maintain fewer entries in the
|
||||
// known_hosts files.
|
||||
hostForSSH := host
|
||||
if v, ok := nodeDNSNameFromArg(st, host); ok {
|
||||
hostForSSH = v
|
||||
}
|
||||
|
||||
ssh, err := exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
// TODO(bradfitz): use Go's crypto/ssh client instead
|
||||
// of failing. But for now:
|
||||
return fmt.Errorf("no system 'ssh' command found: %w", err)
|
||||
}
|
||||
tailscaleBin, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
knownHostsFile, err := writeKnownHosts(st)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
argv := []string{ssh}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
argv = append(argv, "-vvv")
|
||||
}
|
||||
argv = append(argv,
|
||||
// Only trust SSH hosts that we know about.
|
||||
"-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile),
|
||||
"-o", "UpdateHostKeys no",
|
||||
"-o", "StrictHostKeyChecking yes",
|
||||
)
|
||||
|
||||
// TODO(bradfitz): nc is currently broken on macOS:
|
||||
// https://github.com/tailscale/tailscale/issues/4529
|
||||
// So don't use it for now. MagicDNS is usually working on macOS anyway
|
||||
// and they're not in userspace mode, so 'nc' isn't very useful.
|
||||
if runtime.GOOS != "darwin" {
|
||||
argv = append(argv,
|
||||
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
|
||||
tailscaleBin,
|
||||
rootArgs.socket,
|
||||
))
|
||||
}
|
||||
|
||||
// Explicitly rebuild the user@host argument rather than
|
||||
// passing it through. In general, the use of OpenSSH's ssh
|
||||
// binary is a crutch for now. We don't want to be
|
||||
// Hyrum-locked into passing through all OpenSSH flags to the
|
||||
// OpenSSH client forever. We try to make our flags and args
|
||||
// be compatible, but only a subset. The "tailscale ssh"
|
||||
// command should be a simple and portable one. If they want
|
||||
// to use a different one, we'll later be making stock ssh
|
||||
// work well by default too. (doing things like automatically
|
||||
// setting known_hosts, etc)
|
||||
argv = append(argv, username+"@"+hostForSSH)
|
||||
|
||||
argv = append(argv, argRest...)
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Don't use syscall.Exec on Windows.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var ee *exec.ExitError
|
||||
err := cmd.Run()
|
||||
if errors.As(err, &ee) {
|
||||
os.Exit(ee.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("unreachable")
|
||||
}
|
||||
|
||||
func writeKnownHosts(st *ipnstate.Status) (knownHostsFile string, err error) {
|
||||
confDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tsConfDir := filepath.Join(confDir, "tailscale")
|
||||
if err := os.MkdirAll(tsConfDir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
knownHostsFile = filepath.Join(tsConfDir, "ssh_known_hosts")
|
||||
want := genKnownHosts(st)
|
||||
if cur, err := os.ReadFile(knownHostsFile); err != nil || !bytes.Equal(cur, want) {
|
||||
if err := os.WriteFile(knownHostsFile, want, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return knownHostsFile, nil
|
||||
}
|
||||
|
||||
func genKnownHosts(st *ipnstate.Status) []byte {
|
||||
var buf bytes.Buffer
|
||||
for _, k := range st.Peers() {
|
||||
ps := st.Peer[k]
|
||||
for _, hk := range ps.SSH_HostKeys {
|
||||
hostKey := strings.TrimSpace(hk)
|
||||
if strings.ContainsAny(hostKey, "\n\r") { // invalid
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&buf, "%s %s\n", ps.DNSName, hostKey)
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// nodeDNSNameFromArg returns the PeerStatus.DNSName value from a peer
|
||||
// in st that matches the input arg which can be a base name, full
|
||||
// DNS name, or an IP.
|
||||
func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok bool) {
|
||||
if arg == "" {
|
||||
return
|
||||
}
|
||||
argIP, _ := netaddr.ParseIP(arg)
|
||||
for _, ps := range st.Peer {
|
||||
dnsName = ps.DNSName
|
||||
if !argIP.IsZero() {
|
||||
for _, ip := range ps.TailscaleIPs {
|
||||
if ip == argIP {
|
||||
return dnsName, true
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSuffix(arg, "."), strings.TrimSuffix(dnsName, ".")) {
|
||||
return dnsName, true
|
||||
}
|
||||
if base, _, ok := strings.Cut(ps.DNSName, "."); ok && strings.EqualFold(base, arg) {
|
||||
return dnsName, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -30,24 +29,9 @@ var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
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)")
|
||||
@@ -70,14 +54,7 @@ var statusArgs struct {
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected non-flag arguments to 'tailscale status'")
|
||||
}
|
||||
getStatus := tailscale.Status
|
||||
if !statusArgs.peers {
|
||||
getStatus = tailscale.StatusWithoutPeers
|
||||
}
|
||||
st, err := getStatus(ctx)
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
@@ -93,7 +70,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 {
|
||||
@@ -102,7 +79,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()
|
||||
@@ -129,22 +106,28 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
outln(description)
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(st.Health) > 0 {
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
printf("# - %s\n", m)
|
||||
case ipn.Stopped.String():
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
fmt.Println("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
outln()
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
fmt.Println("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -154,19 +137,11 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
var offline string
|
||||
if !ps.Online {
|
||||
offline = "; offline"
|
||||
}
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node" + offline)
|
||||
} else if ps.ExitNodeOption {
|
||||
f("idle; offers exit node" + offline)
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
f("idle" + offline)
|
||||
} else if !ps.Online {
|
||||
f("offline")
|
||||
f("idle")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
@@ -174,17 +149,12 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
} else if ps.ExitNodeOption {
|
||||
f("offers exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
if !ps.Online {
|
||||
f("; offline")
|
||||
}
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
@@ -212,31 +182,10 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
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 != "" {
|
||||
|
||||
@@ -6,13 +6,9 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -21,18 +17,15 @@ import (
|
||||
"sync"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -51,10 +44,8 @@ down").
|
||||
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --auth-key, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
result of an unspecified flag's default value, unless the --reset
|
||||
flag is also used.
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
@@ -67,41 +58,23 @@ func effectiveGOOS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
||||
// a function of the platform it's running on.
|
||||
func acceptRouteDefault(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := newFlagSet("up")
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
|
||||
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
||||
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
@@ -126,7 +99,6 @@ func defaultNetfilterMode() string {
|
||||
}
|
||||
|
||||
type upArgsT struct {
|
||||
qr bool
|
||||
reset bool
|
||||
server string
|
||||
acceptRoutes bool
|
||||
@@ -135,7 +107,6 @@ type upArgsT struct {
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
forceReauth bool
|
||||
forceDaemon bool
|
||||
advertiseRoutes string
|
||||
@@ -143,56 +114,15 @@ type upArgsT struct {
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
authKey string
|
||||
hostname string
|
||||
opUser string
|
||||
json bool
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
v := a.authKeyOrFile
|
||||
if strings.HasPrefix(v, "file:") {
|
||||
file := strings.TrimPrefix(v, "file:")
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
|
||||
//
|
||||
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
|
||||
// providing the link for where to authenticate this client. BackendState would be
|
||||
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
|
||||
// populated if something goes badly wrong.
|
||||
//
|
||||
// When the client is authenticated by having someone visit the AuthURL, a second
|
||||
// JSON block will be output. The AuthURL and QR fields will not be present, the
|
||||
// BackendState and Error fields will give the result of the authentication.
|
||||
// Ex:
|
||||
// {
|
||||
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
|
||||
// "QR": "data:image/png;base64,0123...cdef"
|
||||
// "BackendState": "NeedsLogin"
|
||||
// }
|
||||
// {
|
||||
// "BackendState": "Running"
|
||||
// }
|
||||
//
|
||||
type upOutputJSON struct {
|
||||
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
|
||||
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
|
||||
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
|
||||
Error string `json:",omitempty"` // description of an error
|
||||
}
|
||||
|
||||
func warnf(format string, args ...any) {
|
||||
printf("Warning: "+format+"\n", args...)
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -200,31 +130,17 @@ var (
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func validateViaPrefix(ipp netaddr.IPPrefix) error {
|
||||
if !tsaddr.IsViaPrefix(ipp) {
|
||||
return fmt.Errorf("%v is not a 4-in-6 prefix", ipp)
|
||||
}
|
||||
if ipp.Bits() < (128 - 32) {
|
||||
return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32)
|
||||
}
|
||||
a := ipp.IP().As16()
|
||||
// The first 64 bits of a are the via prefix.
|
||||
// The next 32 bits are the "site ID".
|
||||
// The last 32 bits are the IPv4.
|
||||
// For now, we reserve the top 3 bytes of the site ID,
|
||||
// and only allow users to use site IDs 0-255.
|
||||
siteID := binary.BigEndian.Uint32(a[8:12])
|
||||
if siteID > 0xFF {
|
||||
return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xff or less", ipp, siteID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
if advertiseRoutes != "" {
|
||||
var default4, default6 bool
|
||||
advroutes := strings.Split(advertiseRoutes, ",")
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
@@ -233,11 +149,6 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
if ipp != ipp.Masked() {
|
||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if tsaddr.IsViaPrefix(ipp) {
|
||||
if err := validateViaPrefix(ipp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
@@ -251,7 +162,7 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if advertiseDefaultRoute {
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
@@ -265,23 +176,24 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
}
|
||||
return routes[i].IP().Less(routes[j].IP())
|
||||
})
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
routes, err := calcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
}
|
||||
|
||||
if upArgs.exitNodeIP == "" && upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
if upArgs.exitNodeIP != "" {
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if exitNodeIP == ip {
|
||||
return nil, fmt.Errorf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", upArgs.exitNodeIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
@@ -303,22 +215,11 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
|
||||
if upArgs.exitNodeIP != "" {
|
||||
if err := prefs.SetExitNodeIP(upArgs.exitNodeIP, st); err != nil {
|
||||
var e ipn.ExitNodeLocalIPError
|
||||
if errors.As(err, &e) {
|
||||
return nil, fmt.Errorf("%w; did you mean --advertise-exit-node?", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.RunSSH = upArgs.runSSH
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.Hostname = upArgs.hostname
|
||||
@@ -346,13 +247,12 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
// updatePrefs returns how to edit preferences based on the
|
||||
// flag-provided 'prefs' and the currently active 'curPrefs'.
|
||||
// updatePrefs updates prefs based on curPrefs
|
||||
//
|
||||
// It returns a non-nil justEditMP if we're already running and none of
|
||||
// the flags require a restart, so we can just do an EditPrefs call and
|
||||
// change the prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised routes, etc).
|
||||
// advertised tags, routes, etc).
|
||||
//
|
||||
// It returns simpleUp if we're running a simple "tailscale up" to
|
||||
// transition to running from a previously-logged-in but down state,
|
||||
@@ -372,8 +272,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
@@ -381,19 +279,14 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
|
||||
justEdit := env.backendState == ipn.Running.String() &&
|
||||
!env.upArgs.forceReauth &&
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
|
||||
!env.upArgs.reset &&
|
||||
env.upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
justEditMP.Prefs = *prefs
|
||||
visitFlags := env.flagSet.Visit
|
||||
if env.upArgs.reset {
|
||||
visitFlags = env.flagSet.VisitAll
|
||||
}
|
||||
visitFlags(func(f *flag.Flag) {
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
||||
})
|
||||
}
|
||||
@@ -415,7 +308,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
if upArgs.authKeyOrFile != "" {
|
||||
if upArgs.authKey != "" {
|
||||
// Issue 1755: when using an authkey, don't
|
||||
// show an authURL that might still be pending
|
||||
// from a previous non-completed interactive
|
||||
@@ -459,12 +352,11 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
distro: distro.Get(),
|
||||
user: os.Getenv("USER"),
|
||||
flagSet: upFlagSet,
|
||||
upArgs: upArgs,
|
||||
backendState: st.BackendState,
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
@@ -480,12 +372,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
running := make(chan bool, 1) // gets value once in state ipn.Running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
startingOrRunning := make(chan bool, 1) // gets value once starting or running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
var printed bool // whether we've yet printed anything to stdout or stderr
|
||||
printed := !simpleUp
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
@@ -511,24 +403,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case running <- true:
|
||||
case startingOrRunning <- true:
|
||||
default:
|
||||
}
|
||||
cancel()
|
||||
@@ -536,34 +423,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
log.Printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
@@ -592,17 +452,9 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tailscale.CheckPrefs(ctx, prefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authKey, err := upArgs.getAuthKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: authKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
UpdatePrefs: prefs,
|
||||
}
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
@@ -626,43 +478,21 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// This whole 'up' mechanism is too complicated and results in
|
||||
// hairy stuff like this select. We're ultimately waiting for
|
||||
// 'running' to be done, but even in the case where
|
||||
// it succeeds, other parts may shut down concurrently so we
|
||||
// need to prioritize reads from 'running' if it's
|
||||
// readable; its send does happen before the pump mechanism
|
||||
// shuts down. (Issue 2333)
|
||||
select {
|
||||
case <-running:
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
select {
|
||||
case <-running:
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
||||
data, err := json.MarshalIndent(js, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("printUpDoneJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
)
|
||||
@@ -688,7 +518,6 @@ func init() {
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
addPrefFlagMapping("ssh", "RunSSH")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@@ -706,7 +535,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "auth-key", "force-reauth", "reset", "qr", "json":
|
||||
case "authkey", "force-reauth", "reset":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -740,7 +569,6 @@ type upCheckEnv struct {
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
distro distro.Distro
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
@@ -791,10 +619,6 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
||||
continue
|
||||
}
|
||||
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
|
||||
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
@@ -851,8 +675,8 @@ func flagAppliesToOS(flag, goos string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
ret := make(map[string]any)
|
||||
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interface{}) {
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
exitNodeIPStr := func() string {
|
||||
if !prefs.ExitNodeIP.IsZero() {
|
||||
@@ -869,7 +693,7 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
set := func(v any) {
|
||||
set := func(v interface{}) {
|
||||
if flagAppliesToOS(f.Name, env.goos) {
|
||||
ret[f.Name] = v
|
||||
} else {
|
||||
@@ -879,8 +703,6 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "ssh":
|
||||
set(prefs.RunSSH)
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
@@ -923,7 +745,7 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func fmtFlagValueArg(flagName string, val any) string {
|
||||
func fmtFlagValueArg(flagName string, val interface{}) string {
|
||||
if val == true {
|
||||
return "--" + flagName
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"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,19 @@ 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.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
printf("Client: %s\n", version.String())
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
|
||||
st, err := tailscale.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printf("Daemon: %s\n", st.Version)
|
||||
fmt.Printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1335,14 +1335,3 @@ html {
|
||||
border-color: #6c94ec;
|
||||
border-color: rgba(108, 148, 236, var(--border-opacity));
|
||||
}
|
||||
|
||||
.button-red {
|
||||
background-color: #d04841;
|
||||
border-color: #d04841;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.button-red:enabled:hover {
|
||||
background-color: #b22d30;
|
||||
border-color: #b22d30;
|
||||
}
|
||||
|
||||
@@ -7,27 +7,23 @@ 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"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -53,13 +49,11 @@ func init() {
|
||||
}
|
||||
|
||||
type tmplData struct {
|
||||
Profile tailcfg.UserProfile
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
AdvertiseExitNode bool
|
||||
AdvertiseRoutes string
|
||||
Profile tailcfg.UserProfile
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -76,7 +70,7 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
`),
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := newFlagSet("web")
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
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
|
||||
@@ -89,32 +83,9 @@ var webArgs struct {
|
||||
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)
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
@@ -125,20 +96,8 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
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))
|
||||
}
|
||||
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
|
||||
@@ -269,14 +228,15 @@ func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
// We need a SynoToken for authenticate.cgi.
|
||||
// So we tell the client to get one.
|
||||
_, _ = fmt.Fprint(w, synoTokenRedirectHTML)
|
||||
serverURL := r.URL.Scheme + "://" + r.URL.Host
|
||||
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
|
||||
return true
|
||||
}
|
||||
|
||||
const synoTokenRedirectHTML = `<html><body>
|
||||
Redirecting with session token...
|
||||
<script>
|
||||
var serverURL = window.location.protocol + "//" + window.location.host;
|
||||
var serverURL = %q;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("GET", serverURL + "/webman/login.cgi", true);
|
||||
@@ -306,45 +266,15 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate)
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
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, "{}")
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -353,11 +283,6 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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]
|
||||
@@ -367,18 +292,6 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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()
|
||||
}
|
||||
@@ -392,15 +305,13 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
|
||||
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
|
||||
@@ -447,14 +358,6 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
||||
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
|
||||
@@ -472,15 +375,10 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
||||
bc.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
})
|
||||
if forceReauth {
|
||||
bc.StartLoginInteractive()
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<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>
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -86,30 +86,14 @@
|
||||
<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>
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
{{ end }}
|
||||
</main>
|
||||
<script>(function () {
|
||||
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
||||
let fetchingUrl = false;
|
||||
var data = {
|
||||
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
|
||||
AdvertiseExitNode: advertiseExitNode,
|
||||
Reauthenticate: false
|
||||
};
|
||||
|
||||
function postData(e) {
|
||||
function handleClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (fetchingUrl) {
|
||||
@@ -132,8 +116,7 @@ function postData(e) {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
@@ -151,19 +134,9 @@ function postData(e) {
|
||||
});
|
||||
}
|
||||
|
||||
Array.from(document.querySelectorAll(".js-loginButton")).forEach(el => {
|
||||
el.addEventListener("click", function(e) {
|
||||
data.Reauthenticate = true;
|
||||
postData(e);
|
||||
});
|
||||
Array.from(loginButtons).forEach(el => {
|
||||
el.addEventListener("click", handleClick);
|
||||
})
|
||||
Array.from(document.querySelectorAll(".js-advertiseExitNode")).forEach(el => {
|
||||
el.addEventListener("click", function(e) {
|
||||
data.AdvertiseExitNode = !advertiseExitNode;
|
||||
postData(e);
|
||||
});
|
||||
})
|
||||
|
||||
})();</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -3,21 +3,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/dsnet/golib/jsonfmt from tailscale.com/cmd/tailscale/cli
|
||||
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/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
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+
|
||||
@@ -31,39 +21,31 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
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
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
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/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
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/hostinfo from tailscale.com/net/interfaces
|
||||
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/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/netutil from tailscale.com/client/tailscale
|
||||
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/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/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -81,30 +63,26 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/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/types/wgkey from tailscale.com/types/netmap+
|
||||
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/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
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
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/nacl/box from tailscale.com/derp
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
@@ -116,7 +94,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
@@ -126,7 +104,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from image/png
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -149,7 +127,11 @@ 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
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -160,17 +142,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
@@ -187,26 +166,25 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember+
|
||||
path from html/template+
|
||||
os/user from tailscale.com/util/groupmember
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight+
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
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/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
args = cli.CleanUpArgs(args)
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -27,7 +24,6 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
@@ -39,7 +35,6 @@ import (
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
ifconfig bool // print network state once and exit
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
@@ -50,7 +45,6 @@ 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.")
|
||||
@@ -65,11 +59,8 @@ func debugMode(args []string) error {
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.ifconfig {
|
||||
return runMonitor(ctx, false)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx, true)
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
@@ -80,7 +71,7 @@ func debugMode(args []string) error {
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context, loop bool) error {
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
@@ -97,13 +88,8 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
if loop {
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
}
|
||||
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 {}
|
||||
@@ -142,18 +128,11 @@ func getURL(ctx context.Context, urlStr string) error {
|
||||
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)
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
@@ -197,8 +176,8 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
@@ -228,7 +207,7 @@ func debugPortmap(ctx context.Context) error {
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
|
||||
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
|
||||
@@ -1,362 +1,252 @@
|
||||
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 from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/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
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
github.com/go-multierror/multierror 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/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/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
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/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/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
LD github.com/kr/fs from github.com/pkg/sftp
|
||||
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/netlink from tailscale.com/wgengine/monitor+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
|
||||
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
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
|
||||
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
|
||||
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
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
|
||||
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/mem from tailscale.com/derp+
|
||||
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
|
||||
W 💣 golang.zx2c4.com/wireguard/ipc/winpipe 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/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+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
|
||||
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/tcpip+
|
||||
inet.af/netstack/state/wire from inet.af/netstack/state
|
||||
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
|
||||
inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
|
||||
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack+
|
||||
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
|
||||
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
|
||||
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/tcpconntrack from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
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
|
||||
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/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
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 from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal 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/ipnstate from tailscale.com/ipn+
|
||||
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/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
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/publicdns from tailscale.com/net/dns/resolver
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/wgengine+
|
||||
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/dnsfallback from tailscale.com/control/controlclient
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
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/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
|
||||
tailscale.com/net/socks5/tssocks 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/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
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/pad32 from tailscale.com/net/tstun+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/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/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
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+
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
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/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog 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/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/hkdf from crypto/tls
|
||||
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 golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
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 golang.org/x/net/http2+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
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/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+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
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 from tailscale.com/cmd/tailscaled+
|
||||
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+
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
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/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
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
|
||||
embed from tailscale.com/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -364,21 +254,21 @@ 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+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
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/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
html 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 github.com/godbus/dbus/v5+
|
||||
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+
|
||||
@@ -389,20 +279,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/httputil from tailscale.com/ipn/localapi
|
||||
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/pprof from tailscale.com/cmd/tailscaled
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
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
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
@@ -416,5 +305,5 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
text/tabwriter from runtime/pprof
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf8 from bufio+
|
||||
|
||||
@@ -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
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package main
|
||||
|
||||
// Force registration of tailssh with LocalBackend.
|
||||
import _ "tailscale.com/ssh/tailssh"
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
@@ -23,34 +20,23 @@ import (
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/ipn"
|
||||
"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/socks5/tssocks"
|
||||
"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"
|
||||
@@ -72,27 +58,11 @@ func defaultTunName() string {
|
||||
// as a magic value that uses/creates any free number.
|
||||
return "utun"
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
if distro.Get() == 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"
|
||||
}
|
||||
@@ -103,44 +73,44 @@ var args struct {
|
||||
// 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
|
||||
cleanup bool
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 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
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) 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,
|
||||
}
|
||||
|
||||
func main() {
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
// allocations. So let's be aggressive about garbage collection,
|
||||
// unless the user specifically overrides it in the usual way.
|
||||
if _, ok := os.LookupEnv("GOGC"); !ok {
|
||||
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.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
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")
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
@@ -182,89 +152,30 @@ func main() {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
if args.birdSocketPath != "" && createBIRDClient == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
goos := os.Getenv("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
|
||||
}
|
||||
o.Port = 41112
|
||||
o.StatePath = args.statepath
|
||||
o.SocketPath = args.socketpath // even for goos=="windows", for tests
|
||||
|
||||
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
|
||||
@@ -277,7 +188,7 @@ func ipnServerOpts() (o ipnserver.Options) {
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
pol := logpolicy.New(logtail.CollectionNode)
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
@@ -297,13 +208,13 @@ func run() error {
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if envknob.Bool("TS_DEBUG_MEMORY") {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
if envknob.Bool("TS_PLEASE_PANIC") {
|
||||
if os.Getenv("TS_PLEASE_PANIC") != "" {
|
||||
panic("TS_PLEASE_PANIC asked us to panic")
|
||||
}
|
||||
dns.Cleanup(logf, args.tunname)
|
||||
@@ -311,76 +222,53 @@ func run() error {
|
||||
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)
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
var socksListener net.Listener
|
||||
if args.socksAddr != "" {
|
||||
var err error
|
||||
socksListener, err = net.Listen("tcp", args.socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(args.socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
dialer.Logf = logf
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
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)
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
var ns *netstack.Impl
|
||||
if useNetstack || wrapNetstack {
|
||||
onlySubnets := wrapNetstack && !useNetstack
|
||||
ns = mustStartNetstack(logf, e, onlySubnets)
|
||||
}
|
||||
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))
|
||||
}()
|
||||
}
|
||||
if socksListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
@@ -395,7 +283,6 @@ func run() error {
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
defer dialer.Close()
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
@@ -406,108 +293,67 @@ func run() error {
|
||||
}()
|
||||
|
||||
opts := ipnServerOpts()
|
||||
|
||||
store, err := store.New(logf, statePathOrDefault())
|
||||
if err != nil {
|
||||
return fmt.Errorf("store.New: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
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)
|
||||
opts.DebugMux = debugMux
|
||||
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) {
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (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)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, 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...)
|
||||
return nil, false, multierror.New(errs)
|
||||
}
|
||||
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
|
||||
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
|
||||
v, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
if distro.Get() == distro.Synology {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin", "freebsd":
|
||||
case "windows", "darwin":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
// affect the GUI clients).
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, 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 {
|
||||
if !useNetstack {
|
||||
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)
|
||||
return nil, false, err
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
@@ -519,11 +365,11 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, fmt.Errorf("creating router: %w", err)
|
||||
return nil, false, err
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
return nil, false, err
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
@@ -540,7 +386,6 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
|
||||
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)
|
||||
@@ -549,12 +394,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,
|
||||
@@ -565,68 +404,17 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
||||
log.Fatalf("%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:])
|
||||
ns, err := netstack.Create(logf, tunDev, e, magicConn, onlySubnets)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user