Compare commits

..

1 Commits

Author SHA1 Message Date
Denton Gentry
1dc90404f3 cmd/tailscale{,d}: combine into a single binary
To reduce size, combine tailscaled and tailscale into a single
binary which will figure out what it should do based on argv[0].

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-05-17 08:16:50 -07:00
510 changed files with 13200 additions and 39177 deletions

View File

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

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

@@ -0,0 +1,8 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: 'needs-triage'
assignees: ''
---

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'needs-triage'
assignees: ''
---

View File

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

View File

@@ -1,16 +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:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "go.mod:"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: ".github:"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
name: go generate
on:
push:
branches:
- main
- "release-branch/*"
pull_request:
branches:
- "*"
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
with:
go-version: 1.17
- name: Check out code
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
echo
echo
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)

View File

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

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v1
with:
go-version: 1.17
go-version: 1.16
id: go
- name: Check out code into the Go module directory
@@ -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: |

View File

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

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v1
with:
go-version: 1.17
go-version: 1.16
id: go
- name: Check out code into the Go module directory
@@ -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: |

View File

@@ -14,9 +14,9 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v1
with:
go-version: 1.17
go-version: 1.16
- name: Check out code
uses: actions/checkout@v1
@@ -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:

View File

@@ -1,44 +0,0 @@
name: VM
on:
pull_request:
branches:
- '*'
jobs:
ubuntu2004-LTS-cloud-base:
runs-on: [ self-hosted, linux, vm ]
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.17
id: go
- name: Checkout Code
uses: actions/checkout@v1
- name: Run VM tests
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
env:
HOME: "/tmp"
TMPDIR: "/tmp"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2
with:
go-version: 1.17.x
go-version: 1.16.x
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -17,9 +17,9 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2
with:
go-version: 1.17.x
go-version: 1.16.x
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -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,11 +38,12 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.17-alpine AS build-env
FROM golang:1.16-alpine AS build-env
WORKDIR /go/src/tailscale
COPY go.mod go.sum ./
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
@@ -48,13 +55,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 -tags=xversion -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/

View File

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

View File

@@ -1,5 +1,3 @@
IMAGE_REPO ?= tailscale/tailscale
usage:
echo "See Makefile"
@@ -20,13 +18,7 @@ buildwindows:
build386:
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildlinuxarm:
GOOS=linux GOARCH=arm 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:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)

View File

@@ -43,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.17) 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.

View File

@@ -1 +1 @@
1.19.0
1.9.0

91
api.md
View File

@@ -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,14 +13,11 @@ 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)
* **[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)
- [DNS](#tailnet-dns)
@@ -40,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.
@@ -129,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.
@@ -166,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
@@ -195,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
@@ -236,33 +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.
## 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.
@@ -501,16 +471,15 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
##### Parameters
###### 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.
`user` - A user's email. The provided ACL is queried with this user to determine which rules match.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFor=user1@example.com&type=user' \
POST /api/v2/tailnet/example.com/acl/preiew
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@example.com' \
-u "tskey-yourapikey123:" \
--data-binary '// Example/default ACLs for unrestricted connections.
{
@@ -540,50 +509,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

View File

@@ -11,38 +11,6 @@
set -eu
IFS=".$IFS" read -r major minor patch <VERSION.txt
git_hash=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
git_hash="${git_hash}-dirty"
fi
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
change_count=$(git rev-list --count HEAD "^$base_hash")
short_hash=$(echo "$git_hash" | cut -c1-9)
eval $(./version/version.sh)
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
change_suffix="-$change_count"
else
change_suffix=""
fi
long_suffix="$change_suffix-t$short_hash"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"
EOF
exit 0
fi
exec 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 -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}" "$@"

View File

@@ -8,28 +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
eval $(./build_dist.sh shellvars)
eval $(./version/version.sh)
go run github.com/tailscale/mkctr@latest \
--base="ghcr.io/tailscale/alpine-base:3.14" \
--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}" \
--tags="${VERSION_SHORT},${VERSION_MINOR}" \
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
--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 .

View File

@@ -1,83 +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, bs: bufio.NewScanner(conn)}
// Read and discard the first line as that is the welcome message.
if _, err := b.readLine(); 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
bs *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\n", 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\n", 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)
}
func (b *BIRDClient) exec(cmd string, args ...interface{}) (string, error) {
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
return "", err
}
return b.readLine()
}
func (b *BIRDClient) readLine() (string, error) {
if !b.bs.Scan() {
return "", fmt.Errorf("reading response from bird failed")
}
if err := b.bs.Err(); err != nil {
return "", err
}
return b.bs.Text(), nil
}

View File

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

View File

@@ -8,7 +8,6 @@ package tailscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -17,56 +16,40 @@ import (
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"time"
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// TailscaledSocket is the tailscaled Unix socket.
var TailscaledSocket = paths.DefaultTailscaledSocket()
// 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)
}
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, safesocket.WindowsLocalPort)
// 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.
@@ -77,13 +60,6 @@ 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)
}
@@ -94,20 +70,6 @@ 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 {
@@ -118,23 +80,6 @@ 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 {
@@ -142,29 +87,14 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
}
res, err := DoLocalRequest(req)
if err != nil {
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
pathPrefix := path
if i := strings.Index(path, "?"); i != -1 {
pathPrefix = path[:i]
}
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
defer res.Body.Close()
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != wantStatus {
if res.StatusCode == 403 {
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(slurp))}
}
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
return nil, bestError(err, slurp)
}
@@ -196,24 +126,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)
@@ -344,135 +256,3 @@ func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//
// It ends in a punctuation. See caller.
func tailscaledConnectHint() string {
if runtime.GOOS != "linux" {
// TODO(bradfitz): flesh this out
return "not running?"
}
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
if err != nil {
return "not running?"
}
// Parse:
// LoadState=loaded
// ActiveState=inactive
// SubState=dead
st := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
if i := strings.Index(line, "="); i != -1 {
st[line[:i]] = strings.TrimSpace(line[i+1:])
}
}
if st["LoadState"] == "loaded" &&
(st["SubState"] != "running" || st["ActiveState"] != "active") {
return "systemd tailscaled.service not running."
}
return "not running?"
}

View File

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

View File

@@ -17,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,13 +63,37 @@ 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 ...interface{}) {
@@ -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,89 +167,105 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
return types.TypeString(t, pkgQual)
}
t, ok := typ.Underlying().(*types.Struct)
if !ok {
return
}
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...)
}
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()
if !codegen.ContainsPointers(ft) {
continue
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...)
}
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)
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
writef("}")
} else {
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
}
case *types.Pointer:
if named, _ := ft.Elem().(*types.Named); named != nil && codegen.ContainsPointers(ft.Elem()) {
writef("dst.%s = src.%s.Clone()", fname, fname)
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
}
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")`)
if named, _ := ft.(*types.Named); named != nil && !hasBasicUnderlying(ft) {
writef("dst.%s = *src.%s.Clone()", fname, fname)
continue
}
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}")
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("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("}")
case *types.Map:
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
n := importedName(sliceType.Elem())
writef("\tfor k := range src.%s {", fname)
// use zero-length slice instead of nil to ensure
// the key is always copied.
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
writef("\t}")
} else if containsPointers(ft.Elem()) {
writef("\t\t" + `panic("TODO map value pointers")`)
} else {
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v", fname)
writef("\t}")
}
writef("}")
case *types.Struct:
writef(`panic("TODO struct %s")`, fname)
default:
writef(`panic(fmt.Sprintf("TODO: %T", ft))`)
}
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")
buf.Write(regenBuf.Bytes())
}
}
func hasBasicUnderlying(typ types.Type) bool {
@@ -242,3 +276,34 @@ func hasBasicUnderlying(typ types.Type) bool {
return false
}
}
func containsPointers(typ types.Type) bool {
switch typ.String() {
case "time.Time":
// time.Time contains a pointer that does not need copying
return false
case "inet.af/netaddr.IP":
return false
}
switch ft := typ.Underlying().(type) {
case *types.Array:
return containsPointers(ft.Elem())
case *types.Chan:
return true
case *types.Interface:
return true // a little too broad
case *types.Map:
return true
case *types.Pointer:
return true
case *types.Slice:
return true
case *types.Struct:
for i := 0; i < ft.NumFields(); i++ {
if containsPointers(ft.Field(i).Type()) {
return true
}
}
}
return false
}

View File

@@ -1,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 x509Cert.VerifyHostname(hostname) != nil {
return nil, errors.New("refuse to load cert: hostname mismatch with key")
}
return &manualCertManager{cert: &cert, hostname: hostname}, nil
}
func (m *manualCertManager) TLSConfig() *tls.Config {
return &tls.Config{
Certificates: nil,
NextProtos: []string{
"h2", "http/1.1", // enable HTTP/2
},
GetCertificate: m.getCertificate,
}
}
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi.ServerName != m.hostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}
return m.cert, nil
}
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
return fallback
}

View File

@@ -12,6 +12,8 @@ import (
"errors"
"expvar"
"flag"
"fmt"
"html"
"io"
"io/ioutil"
"log"
@@ -23,6 +25,7 @@ import (
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
"tailscale.com/atomicfile"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -31,13 +34,14 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
"tailscale.com/version"
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
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")
@@ -45,44 +49,18 @@ var (
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.")
)
var (
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")
)
func init() {
stats.Set("counter_requests", stunDisposition)
stats.Set("counter_addrfamily", stunAddrFamily)
expvar.Publish("stun", stats)
}
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 {
*configPath = "/var/lib/derper/derper.key"
} else {
log.Fatalf("derper: -c <config path> not specified")
}
log.Printf("no config path specified; using %s", *configPath)
log.Fatalf("derper: -c <config path> not specified")
}
b, err := ioutil.ReadFile(*configPath)
switch {
@@ -100,13 +78,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 {
@@ -128,11 +114,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)
@@ -141,10 +122,9 @@ func main() {
cfg := loadConfig()
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
letsEncrypt := tsweb.IsProd443(*addr)
s := derp.NewServer(cfg.PrivateKey, log.Printf)
s.SetVerifyClient(*verifyClients)
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
@@ -163,11 +143,9 @@ func main() {
}
expvar.Publish("derp", s.ExpVar())
mux := http.NewServeMux()
derpHandler := derphttp.Handler(s)
derpHandler = addWebSocketSupport(s, derpHandler)
mux.Handle("/derp", derpHandler)
mux.HandleFunc("/derp/probe", probeHandler)
// Create our own mux so we don't expose /debug/ stuff to the world.
mux := tsweb.NewMux(debugHandler(s))
mux.Handle("/derp", derphttp.Handler(s))
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -186,82 +164,43 @@ func main() {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
}
}))
debug := tsweb.Debugger(mux)
debug.KV("TLS hostname", *hostname)
debug.KV("Mesh key", s.HasMeshKey())
debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
}))
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
if *runSTUN {
go serveSTUN(listenHost)
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
}
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 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)
})
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, "80"),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
}
err := port80srv.ListenAndServe()
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
@@ -278,44 +217,78 @@ func main() {
}
}
// probeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func probeHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "HEAD", "GET":
w.Header().Set("Access-Control-Allow-Origin", "*")
default:
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
}
func debugHandler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/debug/check" {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
return
}
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
f(`<html><body>
<h1>DERP debug</h1>
<ul>
`)
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
<li><a href="/debug/pprof/">/debug/pprof/</a></li>
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine</a> (collapsed)</li>
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine</a> (full)</li>
<li><a href="/debug/check">/debug/check</a> internal consistency check</li>
<ul>
</html>
`)
})
}
func serveSTUN(host string) {
pc, err := net.ListenPacket("udp", net.JoinHostPort(host, "3478"))
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)
@@ -332,7 +305,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 {

View File

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

View File

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

View File

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

View File

@@ -1,423 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The derpprobe binary probes derpers.
package main // import "tailscale.com/cmd/derper/derpprobe"
import (
"bytes"
"context"
crand "crypto/rand"
"encoding/json"
"flag"
"fmt"
"html"
"io"
"log"
"net"
"net/http"
"sort"
"sync"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/net/stun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
)
var (
mu sync.Mutex
state = map[nodePair]pairStatus{}
lastDERPMap *tailcfg.DERPMap
lastDERPMapAt time.Time
)
func main() {
flag.Parse()
go probeLoop()
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
}
type overallStatus struct {
good, bad []string
}
func (st *overallStatus) addBadf(format string, a ...interface{}) {
st.bad = append(st.bad, fmt.Sprintf(format, a...))
}
func (st *overallStatus) addGoodf(format string, a ...interface{}) {
st.good = append(st.good, fmt.Sprintf(format, a...))
}
func getOverallStatus() (o overallStatus) {
mu.Lock()
defer mu.Unlock()
if lastDERPMap == nil {
o.addBadf("no DERP map")
return
}
now := time.Now()
if age := now.Sub(lastDERPMapAt); age > time.Minute {
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
}
addPairMeta := func(pair nodePair) {
st, ok := state[pair]
age := now.Sub(st.at).Round(time.Second)
switch {
case !ok:
o.addBadf("no state for %v", pair)
case st.err != nil:
o.addBadf("%v: %v", pair, st.err)
case age > 90*time.Second:
o.addBadf("%v: update is %v old", pair, age)
default:
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
}
}
for _, reg := range sortedRegions(lastDERPMap) {
for _, from := range reg.Nodes {
addPairMeta(nodePair{"UDP", from.Name})
for _, to := range reg.Nodes {
addPairMeta(nodePair{from.Name, to.Name})
}
}
}
return
}
func serve(w http.ResponseWriter, r *http.Request) {
st := getOverallStatus()
summary := "All good"
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 {
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
}
for _, s := range st.good {
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
}
io.WriteString(w, "</ul></body></html>\n")
}
func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
ret := make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, r := range dm.Regions {
ret = append(ret, r)
}
sort.Slice(ret, func(i, j int) bool { return ret[i].RegionID < ret[j].RegionID })
return ret
}
type nodePair struct {
from string // DERPNode.Name, or "UDP" for a STUN query to 'to'
to string // DERPNode.Name
}
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
type pairStatus struct {
err error
latency time.Duration
at time.Time
}
func setDERPMap(dm *tailcfg.DERPMap) {
mu.Lock()
defer mu.Unlock()
lastDERPMap = dm
lastDERPMapAt = time.Now()
}
func setState(p nodePair, latency time.Duration, err error) {
mu.Lock()
defer mu.Unlock()
st := pairStatus{
err: err,
latency: latency,
at: time.Now(),
}
state[p] = st
if err != nil {
log.Printf("%+v error: %v", p, err)
} else {
log.Printf("%+v: %v", p, latency.Round(time.Millisecond))
}
}
func probeLoop() {
ticker := time.NewTicker(15 * time.Second)
for {
err := probe()
if err != nil {
log.Printf("probe: %v", err)
}
<-ticker.C
}
}
func probe() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
dm, err := getDERPMap(ctx)
if err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(len(dm.Regions))
for _, reg := range dm.Regions {
reg := reg
go func() {
defer wg.Done()
for _, from := range reg.Nodes {
latency, err := probeUDP(ctx, dm, from)
setState(nodePair{"UDP", from.Name}, latency, err)
for _, to := range reg.Nodes {
latency, err := probeNodePair(ctx, dm, from, to)
setState(nodePair{from.Name, to.Name}, latency, err)
}
}
}()
}
wg.Wait()
return ctx.Err()
}
func probeUDP(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (latency time.Duration, err error) {
pc, err := net.ListenPacket("udp", ":0")
if err != nil {
return 0, err
}
defer pc.Close()
uc := pc.(*net.UDPConn)
tx := stun.NewTxID()
req := stun.Request(tx)
for _, ipStr := range []string{n.IPv4, n.IPv6} {
if ipStr == "" {
continue
}
port := n.STUNPort
if port == -1 {
continue
}
if port == 0 {
port = 3478
}
for {
ip := net.ParseIP(ipStr)
_, err := uc.WriteToUDP(req, &net.UDPAddr{IP: ip, Port: port})
if err != nil {
return 0, err
}
buf := make([]byte, 1500)
uc.SetReadDeadline(time.Now().Add(2 * time.Second))
t0 := time.Now()
n, _, err := uc.ReadFromUDP(buf)
d := time.Since(t0)
if err != nil {
if ctx.Err() != nil {
return 0, fmt.Errorf("timeout reading from %v: %v", ip, err)
}
if d < time.Second {
return 0, fmt.Errorf("error reading from %v: %v", ip, err)
}
time.Sleep(100 * time.Millisecond)
continue
}
txBack, _, _, err := stun.ParseResponse(buf[:n])
if err != nil {
return 0, fmt.Errorf("parsing STUN response from %v: %v", ip, err)
}
if txBack != tx {
return 0, fmt.Errorf("read wrong tx back from %v", ip)
}
if latency == 0 || d < latency {
latency = d
}
break
}
}
return latency, nil
}
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
// The passed in context is a minute for the whole region. The
// idea is that each node pair in the region will be done
// serially and regularly in the future, reusing connections
// (at least in the happy path). For now they don't reuse
// connections and probe at most once every 15 seconds. We
// bound the duration of a single node pair within a region
// so one bad one can't starve others.
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
fromc, err := newConn(ctx, dm, from)
if err != nil {
return 0, err
}
defer fromc.Close()
toc, err := newConn(ctx, dm, to)
if err != nil {
return 0, err
}
defer toc.Close()
// Wait a bit for from's node to hear about to existing on the
// other node in the region, in the case where the two nodes
// are different.
if from.Name != to.Name {
time.Sleep(100 * time.Millisecond) // pretty arbitrary
}
// Make a random packet
pkt := make([]byte, 8)
crand.Read(pkt)
t0 := time.Now()
// Send the random packet.
sendc := make(chan error, 1)
go func() {
sendc <- fromc.Send(toc.SelfPublicKey(), pkt)
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout sending via %q: %w", from.Name, ctx.Err())
case err := <-sendc:
if err != nil {
return 0, fmt.Errorf("error sending via %q: %w", from.Name, err)
}
}
// Receive the random packet.
recvc := make(chan interface{}, 1) // either derp.ReceivedPacket or error
go func() {
for {
m, err := toc.Recv()
if err != nil {
recvc <- err
return
}
switch v := m.(type) {
case derp.ReceivedPacket:
recvc <- v
default:
log.Printf("%v: ignoring Recv frame type %T", to.Name, v)
// Loop.
}
}
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout receiving from %q: %w", to.Name, ctx.Err())
case v := <-recvc:
if err, ok := v.(error); ok {
return 0, fmt.Errorf("error receiving from %q: %w", to.Name, err)
}
p := v.(derp.ReceivedPacket)
if p.Source != fromc.SelfPublicKey() {
return 0, fmt.Errorf("got data packet from unexpected source, %v", p.Source)
}
if !bytes.Equal(p.Data, pkt) {
return 0, fmt.Errorf("unexpected data packet %q", p.Data)
}
}
return time.Since(t0), nil
}
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
priv := key.NewNode()
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
rid := n.RegionID
return &tailcfg.DERPRegion{
RegionID: rid,
RegionCode: fmt.Sprintf("%s-%s", dm.Regions[rid].RegionCode, n.Name),
RegionName: dm.Regions[rid].RegionName,
Nodes: []*tailcfg.DERPNode{n},
}
})
dc.IsProber = true
err := dc.Connect(ctx)
if err != nil {
return nil, err
}
errc := make(chan error, 1)
go func() {
m, err := dc.Recv()
if err != nil {
errc <- err
return
}
switch m.(type) {
case derp.ServerInfoMessage:
errc <- nil
default:
errc <- fmt.Errorf("unexpected first message type %T", errc)
}
}()
select {
case err := <-errc:
if err != nil {
go dc.Close()
return nil, err
}
case <-ctx.Done():
go dc.Close()
return nil, fmt.Errorf("timeout waiting for ServerInfoMessage: %w", ctx.Err())
}
return dc, nil
}
var httpOrFileClient = &http.Client{Transport: httpOrFileTransport()}
func httpOrFileTransport() http.RoundTripper {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
return tr
}
func getDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", *derpMapURL, nil)
if err != nil {
return nil, err
}
res, err := httpOrFileClient.Do(req)
if err != nil {
mu.Lock()
defer mu.Unlock()
if lastDERPMap != nil && time.Since(lastDERPMapAt) < 10*time.Minute {
// Assume that control is restarting and use
// the same one for a bit.
return lastDERPMap, nil
}
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetching %s: %s", *derpMapURL, res.Status)
}
dm := new(tailcfg.DERPMap)
if err := json.NewDecoder(res.Body).Decode(dm); err != nil {
return nil, fmt.Errorf("decoding %s JSON: %v", *derpMapURL, err)
}
setDERPMap(dm)
return dm, nil
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,156 +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"
"runtime"
"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 ...interface{}) {
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 tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale cert' or 'tailscale up --operator=$USER' to not require root.", err)
}
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\n", dir)
}
if certArgs.certFile != "" {
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
if err != nil {
return err
}
if certArgs.certFile != "-" {
macWarn()
if certChanged {
printf("Wrote public cert to %v\n", certArgs.certFile)
} else {
printf("Public cert unchanged at %v\n", certArgs.certFile)
}
}
}
if certArgs.keyFile != "" {
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
if err != nil {
return err
}
if certArgs.keyFile != "-" {
macWarn()
if keyChanged {
printf("Wrote private key to %v\n", certArgs.keyFile)
} else {
printf("Private key unchanged at %v\n", certArgs.keyFile)
}
}
}
return nil
}
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
if filename == "-" {
Stdout.Write(contents)
return false, nil
}
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
return false, nil
}
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
return false, err
}
return true, nil
}

View File

@@ -1,318 +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 (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"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.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
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: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale local API",
},
{
Name: "prefs",
Exec: runPrefs,
ShortHelp: "print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
return fs
})(),
},
{
Name: "watch-ipn",
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
return fs
})(),
},
},
}
var debugArgs struct {
file string
cpuSec int
cpuFile string
memFile string
}
func writeProfile(dst string, v []byte) error {
if dst == "-" {
_, err := Stdout.Write(v)
return err
}
return os.WriteFile(dst, v, 0600)
}
func outName(dst string) string {
if dst == "-" {
return "stdout"
}
if runtime.GOOS == "darwin" {
return fmt.Sprintf("%s (warning: sandboxed macOS binaries write to Library/Containers; use - to write to stdout and redirect to file instead)", dst)
}
return dst
}
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 out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing memory profile ...")
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
return err
}
log.Printf("Memory profile written to %s", outName(out))
}
}
if debugArgs.file != "" {
usedFlag = true // TODO(bradfitz): add "file" subcommand
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
fatalf("%v\n", err)
}
e := json.NewEncoder(Stdout)
e.SetIndent("", "\t")
e.Encode(wfs)
return nil
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
}
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
if err != nil {
return err
}
log.Printf("Size: %v\n", size)
io.Copy(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 runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)
}
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,10 @@
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
@@ -24,29 +12,22 @@ 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+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
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/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/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp
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/kube from tailscale.com/ipn
💣 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/netknob from tailscale.com/net/netns
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
@@ -54,42 +35,38 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck
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+
L tailscale.com/util/lineread from tailscale.com/net/interfaces
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro 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/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/dns/dnsmessage from net
golang.org/x/net/http/httpguts from net/http+
@@ -98,12 +75,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
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+
@@ -113,7 +90,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+
@@ -136,28 +113,28 @@ 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+
encoding from encoding/json+
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+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from tailscale.com/cmd/tailscale/cli+
errors from bufio+
expvar from tailscale.com/derp+
flag from github.com/peterbourgon/ff/v3+
flag from github.com/peterbourgon/ff/v2+
fmt from compress/flate+
hash from crypto+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate+
html/template from tailscale.com/cmd/tailscale/cli
image from github.com/skip2/go-qrcode+
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
io from bufio+
io/fs from crypto/rand+
io/ioutil from golang.org/x/sys/cpu+
@@ -179,11 +156,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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+
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
sort from compress/flate+
@@ -192,7 +168,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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+

View File

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

View File

@@ -19,11 +19,10 @@ 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/ipn"
"tailscale.com/paths"
@@ -31,22 +30,6 @@ import (
"tailscale.com/syncs"
)
var Stderr io.Writer = os.Stderr
var Stdout io.Writer = os.Stdout
func printf(format string, a ...interface{}) {
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 ...interface{}) {
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.
@@ -93,30 +76,13 @@ 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
}
// Run runs the CLI. The args do not include the binary name.
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{
@@ -141,7 +107,6 @@ change in the future.
webCmd,
fileCmd,
bugReportCmd,
certCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
@@ -157,33 +122,23 @@ change in the future.
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
tailscale.TailscaledSocket = rootArgs.socket
err := rootCmd.Run(context.Background())
if errors.Is(err, flag.ErrHelp) {
if err == flag.ErrHelp {
return nil
}
return err
}
func fatalf(format string, a ...interface{}) {
if Fatalf != nil {
Fatalf(format, a...)
return
}
log.SetFlags(0)
log.Fatalf(format, a...)
}
// Fatalf, if non-nil, is used instead of log.Fatalf.
var Fatalf func(format string, a ...interface{})
var rootArgs struct {
socket string
}
@@ -191,7 +146,7 @@ var rootArgs struct {
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
c, err := safesocket.Connect(rootArgs.socket, 41112)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")

View File

@@ -13,14 +13,10 @@ import (
"strings"
"testing"
"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.
@@ -58,7 +54,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
}{
@@ -407,60 +402,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "ignore_login_server_synonym",
flags: []string{"--login-server=https://controlplane.tailscale.com"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "", // not an error
},
{
name: "ignore_login_server_synonym_on_other_change",
flags: []string{"--netfilter-mode=off"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: false,
NetfilterMode: preftype.NetfilterOn,
},
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: distro.Synology,
want: "",
},
{
// Same test case as "synology_permit_omit_accept_routes" above, but
// on non-Synology distro.
name: "not_synology_dont_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: "", // not Synology
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -477,11 +418,9 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
}
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
var got string
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
if err := checkForAccidentalSettingReverts(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
distro: tt.distro,
}); err != nil {
got = err.Error()
}
@@ -643,7 +582,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
}
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"
@@ -652,7 +594,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 {
@@ -724,120 +666,3 @@ func TestFlagAppliesToOS(t *testing.T) {
})
}
}
func TestUpdatePrefs(t *testing.T) {
tests := []struct {
name string
flags []string // argv to be parsed into env.flagSet and env.upArgs
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
wantSimpleUp bool
wantJustEditMP *ipn.MaskedPrefs
wantErrSubtr string
}{
{
name: "bare_up_means_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
},
},
{
name: "just_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{
backendState: "Stopped",
},
wantSimpleUp: true,
},
{
name: "just_edit",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "control_synonym",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "change_login_server",
flags: []string{"--login-server=https://localhost:1000"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
wantErrSubtr: "can't change --login-server without --force-reauth",
},
{
name: "change_tags",
flags: []string{"--advertise-tags=tag:foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
tt.env.flagSet.Parse(tt.flags)
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
t.Fatal(err)
}
simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
if err != nil {
if tt.wantErrSubtr != "" {
if !strings.Contains(err.Error(), tt.wantErrSubtr) {
t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
}
return
}
t.Fatal(err)
}
if simpleUp != tt.wantSimpleUp {
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
}
if justEditMP != nil {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
}
})
}
}

129
cmd/tailscaled/cli/debug.go Normal file
View File

@@ -0,0 +1,129 @@
// 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"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"runtime"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
FlagSet: (func() *flag.FlagSet {
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.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")
return fs
})(),
}
var debugArgs struct {
localCreds bool
goroutines bool
ipn bool
netMap bool
file string
prefs bool
pretty bool
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
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 debugArgs.prefs {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
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.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 != "" {
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
log.Fatal(err)
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
e.Encode(wfs)
return nil
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
}
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
if err != nil {
return err
}
log.Printf("Size: %v\n", size)
io.Copy(os.Stdout, rc)
return nil
}
return nil
}

View File

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

View File

@@ -23,7 +23,7 @@ import (
"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"
@@ -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")
@@ -74,6 +74,7 @@ func runCp(ctx context.Context, args []string) error {
return runCpTargets(ctx, args)
}
if len(args) < 2 {
//lint:ignore ST1005 no sorry need that colon at the end
return errors.New("usage: tailscale file cp <files...> <target>:")
}
files, target := args[:len(args)-1], args[len(args)-1]
@@ -91,17 +92,19 @@ 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
}
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
peerAPIBase, lastSeen, 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)
} else if !lastSeen.IsZero() && time.Since(lastSeen) > lastSeenOld {
fmt.Fprintf(os.Stderr, "# warning: %s last seen %v ago\n", target, time.Since(lastSeen).Round(time.Minute))
}
if len(files) > 1 {
@@ -172,21 +175,21 @@ func runCp(ctx context.Context, args []string) error {
res.Body.Close()
continue
}
io.Copy(Stdout, res.Body)
io.Copy(os.Stdout, res.Body)
res.Body.Close()
return errors.New(res.Status)
}
return nil
}
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSeen time.Time, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", false, err
return "", time.Time{}, false, err
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return "", false, err
return "", time.Time{}, false, err
}
for _, ft := range fts {
n := ft.Node
@@ -194,11 +197,14 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
if a.IP() != ip {
continue
}
if n.LastSeen != nil {
lastSeen = *n.LastSeen
}
isOffline = n.Online != nil && !*n.Online
return ft.PeerAPIURL, isOffline, nil
return ft.PeerAPIURL, lastSeen, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)
return "", time.Time{}, false, fileTargetErrorDetail(ctx, ip)
}
// fileTargetErrorDetail returns a non-nil error saying why ip is an
@@ -268,6 +274,8 @@ func (r *slowReader) Read(p []byte) (n int, err error) {
return
}
const lastSeenOld = 20 * time.Minute
func runCpTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
@@ -293,7 +301,7 @@ 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)
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
}
return nil
}
@@ -304,7 +312,7 @@ var fileGetCmd = &ffcli.Command{
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.verbose, "verbose", false, "verbose output")
return fs
@@ -415,7 +423,7 @@ func waitForFile(ctx context.Context) error {
fileWaiting := make(chan bool, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
fatalf("Notify.ErrMessage: %v\n", *n.ErrMessage)
log.Fatal(*n.ErrMessage)
}
if n.FilesWaiting != nil {
select {

View File

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

View File

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

View File

@@ -9,18 +9,14 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/net/netcheck"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
@@ -33,7 +29,7 @@ var netcheckCmd = &ffcli.Command{
ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netcheck")
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
@@ -50,7 +46,7 @@ var netcheckArgs struct {
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
@@ -60,20 +56,10 @@ func runNetcheck(ctx context.Context, args []string) error {
}
if strings.HasPrefix(netcheckArgs.format, "json") {
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm, err := tailscale.CurrentDERPMap(ctx)
noRegions := dm != nil && len(dm.Regions) == 0
if noRegions {
log.Printf("No DERP map from tailscaled; using default.")
}
if err != nil || noRegions {
dm, err = prodDERPMap(ctx, http.DefaultClient)
if err != nil {
return err
}
}
dm := derpmap.Prod()
for {
t0 := time.Now()
report, err := c.GetReport(ctx, dm)
@@ -82,7 +68,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 +98,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 +154,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
if netcheckArgs.verbose {
derpNum = fmt.Sprintf("derp%d, ", rid)
}
printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
}
}
return nil
@@ -190,27 +176,3 @@ func portMapping(r *netcheck.Report) string {
}
return strings.Join(got, ", ")
}
func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return nil, fmt.Errorf("create prodDERPMap request: %w", err)
}
res, err := httpc.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b)
}
var derpMap tailcfg.DERPMap
if err = json.Unmarshal(b, &derpMap); err != nil {
return nil, fmt.Errorf("fetch prodDERPMap: %w", err)
}
return &derpMap, nil
}

View File

@@ -14,7 +14,7 @@ import (
"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"
@@ -45,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)")
@@ -74,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
@@ -84,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)
@@ -105,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)
@@ -132,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
}
@@ -155,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
}
}

View File

@@ -14,8 +14,9 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
@@ -31,7 +32,7 @@ var statusCmd = &ffcli.Command{
ShortHelp: "Show state of tailscaled and its connections",
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)")
@@ -56,12 +57,12 @@ var statusArgs struct {
func runStatus(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
return err
}
if statusArgs.json {
if statusArgs.active {
for peer, ps := range st.Peer {
if !ps.Active {
if !peerActive(ps) {
delete(st.Peer, peer)
}
}
@@ -70,7 +71,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 {
@@ -79,7 +80,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()
@@ -108,35 +109,28 @@ func runStatus(ctx context.Context, args []string) error {
switch st.BackendState {
default:
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
os.Exit(1)
case ipn.Stopped.String():
outln("Tailscale is stopped.")
fmt.Println("Tailscale is stopped.")
os.Exit(1)
case ipn.NeedsLogin.String():
outln("Logged out.")
fmt.Println("Logged out.")
if st.AuthURL != "" {
printf("\nLog in at: %s\n", st.AuthURL)
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
}
os.Exit(1)
case ipn.NeedsMachineAuth.String():
outln("Machine is not yet authorized by tailnet admin.")
fmt.Println("Machine is not yet authorized by tailnet admin.")
os.Exit(1)
case ipn.Running.String(), ipn.Starting.String():
case ipn.Running.String():
// Run below.
}
if len(st.Health) > 0 {
printf("# Health check:\n")
for _, m := range st.Health {
printf("# - %s\n", m)
}
outln()
}
var buf bytes.Buffer
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) {
active := peerActive(ps)
f("%-15s %-20s %-12s %-7s ",
firstIPString(ps.TailscaleIPs),
dnsOrQuoteHostname(st, ps),
@@ -145,7 +139,7 @@ func runStatus(ctx context.Context, args []string) error {
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
if !ps.Active {
if !active {
if ps.ExitNode {
f("idle; exit node")
} else if anyTraffic {
@@ -184,16 +178,24 @@ func runStatus(ctx context.Context, args []string) error {
}
ipnstate.SortPeers(peers)
for _, ps := range peers {
if statusArgs.active && !ps.Active {
active := peerActive(ps)
if statusArgs.active && !active {
continue
}
printPS(ps)
}
}
Stdout.Write(buf.Bytes())
os.Stdout.Write(buf.Bytes())
return nil
}
// peerActive reports whether ps has recent activity.
//
// TODO: have the server report this bool instead.
func peerActive(ps *ipnstate.PeerStatus) bool {
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {

View File

@@ -9,7 +9,6 @@ import (
"errors"
"flag"
"fmt"
"log"
"os"
"reflect"
"runtime"
@@ -18,8 +17,7 @@ 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"
@@ -53,19 +51,11 @@ flag is also used.
Exec: runUp,
}
func effectiveGOOS() string {
if v := os.Getenv("TS_DEBUG_UP_FLAG_GOOS"); v != "" {
return v
}
return runtime.GOOS
}
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
var upFlagSet = newUpFlagSet(runtime.GOOS, &upArgs)
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.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
@@ -73,13 +63,13 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
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 IP of the exit node 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")
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.authKeyOrFile, "authkey", "", `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.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\")")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
if safesocket.GOOSUsesPeerCreds(goos) {
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
@@ -102,7 +92,6 @@ func defaultNetfilterMode() string {
}
type upArgsT struct {
qr bool
reset bool
server string
acceptRoutes bool
@@ -118,28 +107,15 @@ type upArgsT struct {
advertiseTags string
snat bool
netfilterMode string
authKeyOrFile string // "secret" or "file:/path/to/secret"
authKey string
hostname string
opUser string
}
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
func warnf(format string, args ...interface{}) {
printf("Warning: "+format+"\n", args...)
fmt.Printf("Warning: "+format+"\n", args...)
}
var (
@@ -147,11 +123,17 @@ var (
ipv6default = netaddr.MustParseIPPrefix("::/0")
)
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 {
@@ -173,7 +155,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
}
@@ -187,20 +169,6 @@ 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 != "" {
@@ -262,9 +230,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
case "off":
prefs.NetfilterMode = preftype.NetfilterOff
if defaultNetfilterMode() != "off" {
warnf("netfilter=off; configure iptables yourself.")
}
warnf("netfilter=off; configure iptables yourself.")
default:
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
}
@@ -272,56 +238,6 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
return prefs, nil
}
// 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).
//
// It returns simpleUp if we're running a simple "tailscale up" to
// transition to running from a previously-logged-in but down state,
// without changing any settings.
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
if !env.upArgs.reset {
applyImplicitPrefs(prefs, curPrefs, env.user)
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
return false, nil, err
}
}
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL &&
!(ipn.IsLoginServerSynonym(curPrefs.ControlURL) && ipn.IsLoginServerSynonym(prefs.ControlURL))
if controlURLChanged && env.backendState == ipn.Running.String() && !env.upArgs.forceReauth {
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 != "" &&
env.backendState != ipn.NeedsLogin.String()
justEdit := env.backendState == ipn.Running.String() &&
!env.upArgs.forceReauth &&
!env.upArgs.reset &&
env.upArgs.authKeyOrFile == "" &&
!controlURLChanged &&
!tagsChanged
if justEdit {
justEditMP = new(ipn.MaskedPrefs)
justEditMP.WantRunningSet = true
justEditMP.Prefs = *prefs
env.flagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
})
}
return simpleUp, justEditMP, nil
}
func runUp(ctx context.Context, args []string) error {
if len(args) > 0 {
fatalf("too many non-flag arguments: %q", args)
@@ -329,14 +245,14 @@ func runUp(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
fatalf("can't fetch status from tailscaled: %v", err)
}
origAuthURL := st.AuthURL
// printAuthURL reports whether we should print out the
// provided auth URL from an IPN notify.
printAuthURL := func(url string) bool {
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
@@ -350,7 +266,7 @@ func runUp(ctx context.Context, args []string) error {
}
if distro.Get() == distro.Synology {
notSupported := "not supported on Synology; see https://github.com/tailscale/tailscale/issues/1995"
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
@@ -362,7 +278,7 @@ func runUp(ctx context.Context, args []string) error {
}
}
prefs, err := prefsFromUpArgs(upArgs, warnf, st, effectiveGOOS())
prefs, err := prefsFromUpArgs(upArgs, warnf, st, runtime.GOOS)
if err != nil {
fatalf("%s", err)
}
@@ -378,31 +294,58 @@ func runUp(ctx context.Context, args []string) error {
return err
}
env := upCheckEnv{
goos: effectiveGOOS(),
distro: distro.Get(),
user: os.Getenv("USER"),
flagSet: upFlagSet,
upArgs: upArgs,
backendState: st.BackendState,
curExitNodeIP: exitNodeIP(prefs, st),
if !upArgs.reset {
applyImplicitPrefs(prefs, curPrefs, os.Getenv("USER"))
if err := checkForAccidentalSettingReverts(upFlagSet, curPrefs, prefs, upCheckEnv{
goos: runtime.GOOS,
curExitNodeIP: exitNodeIP(prefs, st),
}); err != nil {
fatalf("%s", err)
}
}
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil {
fatalf("%s", err)
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL
if controlURLChanged && st.BackendState == ipn.Running.String() && !upArgs.forceReauth {
fatalf("can't change --login-server without --force-reauth")
}
if justEditMP != nil {
_, err := tailscale.EditPrefs(ctx, justEditMP)
// If we're already running and none of the flags require a
// restart, we can just do an EditPrefs call and change the
// prefs at runtime (e.g. changing hostname, changing
// advertised tags, routes, etc)
justEdit := st.BackendState == ipn.Running.String() &&
!upArgs.forceReauth &&
!upArgs.reset &&
upArgs.authKey == "" &&
!controlURLChanged
if justEdit {
mp := new(ipn.MaskedPrefs)
mp.WantRunningSet = true
mp.Prefs = *prefs
upFlagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpFlag(mp, f.Name)
})
_, err := tailscale.EditPrefs(ctx, mp)
return err
}
// simpleUp is whether we're running a simple "tailscale up"
// to transition to running from a previously-logged-in but
// down state, without changing any settings.
simpleUp := upFlagSet.NFlag() == 0 &&
curPrefs.Persist != nil &&
curPrefs.Persist.LoginName != "" &&
st.BackendState != ipn.NeedsLogin.String()
// At this point we need to subscribe to the IPN bus to watch
// for state transitions and possible need to authenticate.
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) }()
@@ -420,7 +363,7 @@ func runUp(ctx context.Context, args []string) error {
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch effectiveGOOS() {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
@@ -436,15 +379,15 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive()
case ipn.NeedsMachineAuth:
printed = true
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
case ipn.Running:
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
case ipn.Starting, ipn.Running:
// Done full authentication process
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()
@@ -452,16 +395,7 @@ func runUp(ctx context.Context, args []string) error {
}
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
printed = true
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
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
@@ -490,13 +424,9 @@ func runUp(ctx context.Context, args []string) error {
return err
}
} else {
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
@@ -507,7 +437,7 @@ func runUp(ctx context.Context, args []string) error {
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection identity. So for now, just
// do as the Windows GUI's always done:
if effectiveGOOS() == "windows" {
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
@@ -520,29 +450,17 @@ 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
}
}
@@ -589,7 +507,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref.
func preflessFlag(flagName string) bool {
switch flagName {
case "authkey", "force-reauth", "reset", "qr":
case "authkey", "force-reauth", "reset":
return true
}
return false
@@ -618,12 +536,7 @@ const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires
// needed by checkForAccidentalSettingReverts and friends.
type upCheckEnv struct {
goos string
user string
flagSet *flag.FlagSet
upArgs upArgsT
backendState string
curExitNodeIP netaddr.IP
distro distro.Distro
}
// checkForAccidentalSettingReverts (the "up checker") checks for
@@ -640,14 +553,14 @@ type upCheckEnv struct {
//
// mp is the mask of settings actually set, where mp.Prefs is the new
// preferences to set, including any values set from implicit flags.
func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheckEnv) error {
func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs *ipn.Prefs, env upCheckEnv) error {
if curPrefs.ControlURL == "" {
// Don't validate things on initial "up" before a control URL has been set.
return nil
}
flagIsSet := map[string]bool{}
env.flagSet.Visit(func(f *flag.Flag) {
flagSet.Visit(func(f *flag.Flag) {
flagIsSet[f.Name] = true
})
@@ -671,13 +584,6 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
if reflect.DeepEqual(valCur, valNew) {
continue
}
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 {
@@ -688,7 +594,7 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
// Compute the stringification of the explicitly provided args in flagSet
// to prepend to the command to run.
var explicit []string
env.flagSet.Visit(func(f *flag.Flag) {
flagSet.Visit(func(f *flag.Flag) {
type isBool interface {
IsBoolFlag() bool
}

View File

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

View File

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

327
cmd/tailscaled/cli/web.go Normal file
View File

@@ -0,0 +1,327 @@
// 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"
_ "embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"log"
"net/http"
"net/http/cgi"
"os/exec"
"runtime"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
"tailscale.com/version/distro"
)
//go:embed web.html
var webHTML string
//go:embed web.css
var webCSS string
var tmpl *template.Template
func init() {
tmpl = template.Must(template.New("web.html").Parse(webHTML))
template.Must(tmpl.New("web.css").Parse(webCSS))
}
type tmplData struct {
Profile tailcfg.UserProfile
SynologyUser string
Status string
DeviceName string
IP string
}
var webCmd = &ffcli.Command{
Name: "web",
ShortUsage: "web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
FlagSet: (func() *flag.FlagSet {
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
})(),
Exec: runWeb,
}
var webArgs struct {
listen string
cgi bool
}
func runWeb(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
}
if webArgs.cgi {
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
log.Printf("tailscale.cgi: %v", err)
return err
}
return nil
}
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
func auth() (string, error) {
if distro.Get() == distro.Synology {
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("auth: %v: %s", err, out)
}
return string(out), nil
}
return "", nil
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
if distro.Get() != distro.Synology {
return false
}
if r.Header.Get("X-Syno-Token") != "" {
return false
}
if r.URL.Query().Get("SynoToken") != "" {
return false
}
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
return false
}
// We need a SynoToken for authenticate.cgi.
// So we tell the client to get one.
serverURL := r.URL.Scheme + "://" + r.URL.Host
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
return true
}
const synoTokenRedirectHTML = `<html><body>
Redirecting with session token...
<script>
var serverURL = %q;
var req = new XMLHttpRequest();
req.overrideMimeType("application/json");
req.open("GET", serverURL + "/webman/login.cgi", true);
req.onload = function() {
var jsonResponse = JSON.parse(req.responseText);
var token = jsonResponse["SynoToken"];
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token;
};
req.send(null);
</script>
</body></html>
`
const authenticationRedirectHTML = `
<html>
<head>
<title>Redirecting...</title>
<style>
html,
body {
height: 100%;
}
html {
background-color: rgb(249, 247, 246);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner {
margin-bottom: 2rem;
border: 4px rgba(112, 110, 109, 0.5) solid;
border-left-color: transparent;
border-radius: 9999px;
width: 4rem;
height: 4rem;
-webkit-animation: spin 700ms linear infinite;
animation: spin 800ms linear infinite;
}
.label {
color: rgb(112, 110, 109);
padding-left: 0.4rem;
}
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="spinner"></div>
<div class="label">Redirecting...</div>
</body>
`
func webHandler(w http.ResponseWriter, r *http.Request) {
if synoTokenRedirect(w, r) {
return
}
user, err := auth()
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
w.Write([]byte(authenticationRedirectHTML))
return
}
if r.Method == "POST" {
type mi map[string]interface{}
w.Header().Set("Content-Type", "application/json")
url, err := tailscaleUpForceReauth(r.Context())
if err != nil {
json.NewEncoder(w).Encode(mi{"error": err})
return
}
json.NewEncoder(w).Encode(mi{"url": url})
return
}
st, err := tailscale.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0]
data := tmplData{
SynologyUser: user,
Profile: profile,
Status: st.BackendState,
DeviceName: deviceName,
}
if len(st.TailscaleIPs) != 0 {
data.IP = st.TailscaleIPs[0].String()
}
buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write(buf.Bytes())
}
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
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
}
st, err := tailscale.Status(ctx)
if err != nil {
return "", fmt.Errorf("can't fetch status: %v", err)
}
origAuthURL := st.AuthURL
// printAuthURL reports whether we should print out the
// provided auth URL from an IPN notify.
printAuthURL := func(url string) bool {
return url != origAuthURL
}
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
go pump(pumpCtx, bc, c)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.Engine != nil {
select {
case gotEngineUpdate <- true:
default:
}
}
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
authURL = *url
cancel()
}
})
// Wait for backend client to be connected so we know
// we're subscribed to updates. Otherwise we can miss
// an update upon its transition to running. Do so by causing some traffic
// back to the bus that we then wait on.
bc.RequestEngineStatus()
select {
case <-gotEngineUpdate:
case <-pumpCtx.Done():
return authURL, pumpCtx.Err()
}
bc.SetPrefs(prefs)
bc.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
})
bc.StartLoginInteractive()
if authURL == "" && retErr == nil {
return "", fmt.Errorf("login failed with no backend error message")
}
return authURL, retErr
}

View File

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

View File

@@ -11,44 +11,33 @@ import (
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/ipn"
"tailscale.com/derp/derpmap"
"tailscale.com/net/interfaces"
"tailscale.com/net/portmapper"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
ifconfig bool // print network state once and exit
monitor bool
getURL string
derpCheck string
portmap bool
}
var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
@@ -61,14 +50,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)
}
if debugArgs.portmap {
return debugPortmap(ctx)
return runMonitor(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
@@ -76,7 +59,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)
@@ -93,13 +76,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 {}
@@ -138,18 +116,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 {
@@ -160,26 +131,7 @@ func getURL(ctx context.Context, urlStr string) error {
}
func checkDerp(ctx context.Context, derpRegion string) error {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return fmt.Errorf("create derp map request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
}
var dmap tailcfg.DERPMap
if err = json.Unmarshal(b, &dmap); err != nil {
return fmt.Errorf("fetch DERP map: %w", err)
}
dmap := derpmap.Prod()
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
@@ -193,8 +145,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)
@@ -218,97 +170,3 @@ func checkDerp(ctx context.Context, derpRegion string) error {
log.Printf("ok")
return err
}
func debugPortmap(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
logf := log.Printf
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
}
logf("cb: no mapping")
})
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
return err
}
gatewayAndSelfIP := func() (gw, self netaddr.IP, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netaddr.MustParseIP(v[:i])
self = netaddr.MustParseIP(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()
}
c.SetGatewayLookupFunc(gatewayAndSelfIP)
gw, selfIP, ok := gatewayAndSelfIP()
if !ok {
logf("no gateway or self IP; %v", linkMon.InterfaceState())
return nil
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
}
logf("Probe: %+v", res)
if !res.PCP && !res.PMP && !res.UPnP {
logf("no portmapping services available")
return nil
}
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {
logf("no mapping")
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -1,153 +1,67 @@
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
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/aws
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/aws
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/aws
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
W 💣 github.com/github/certstore from tailscale.com/control/controlclient
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/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/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress from github.com/klauspost/compress/zstd
L github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/snappy from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
W github.com/pkg/errors from github.com/tailscale/certstore
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
W github.com/pkg/errors from github.com/github/certstore
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
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/control/controlclient+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
W 💣 golang.zx2c4.com/wireguard/ipc/namedpipe from golang.zx2c4.com/wireguard/ipc
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
inet.af/netaddr from inet.af/wf+
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
inet.af/netstack/context from inet.af/netstack/refs+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/control/controlclient+
💣 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/refs from inet.af/netstack/refsvfs2
inet.af/netstack/refsvfs2 from inet.af/netstack/tcpip/stack
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
💣 inet.af/netstack/state from inet.af/netstack/atomicbitops+
💣 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 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/internal/tcp from inet.af/netstack/tcpip/stack+
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/net/tstun+
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 from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/transport/internal/network from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/internal/noop from inet.af/netstack/tcpip/transport/raw
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+
@@ -155,121 +69,101 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/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/derp/derpmap from tailscale.com/cmd/tailscaled+
tailscale.com/disco from tailscale.com/derp+
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/internal/deephash from tailscale.com/ipn/ipnlocal+
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/aws from tailscale.com/ipn/ipnserver
tailscale.com/kube from tailscale.com/ipn
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/logpolicy+
tailscale.com/logtail/backoff 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/resolver from tailscale.com/net/dns+
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/netknob from tailscale.com/ipn/localapi+
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/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/net/socks5/tssocks
tailscale.com/net/socks5/tssocks 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/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/paths from tailscale.com/client/tailscale+
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+
💣 tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
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/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/util/clientmetric from tailscale.com/ipn/localapi+
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/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
LW tailscale.com/util/endian from tailscale.com/net/netns+
L 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+
W tailscale.com/wf 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+
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/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
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/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.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -277,17 +171,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from github.com/tailscale/goupnp/httpu+
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
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
@@ -299,6 +193,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
bytes from bufio+
compress/flate from compress/gzip+
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+
@@ -319,10 +214,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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 tailscale.com/net/dns+
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
L embed from tailscale.com/net/dns
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -330,19 +229,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
fmt from compress/flate+
hash from crypto+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from inet.af/netstack/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+
math from compress/flate+
math/big from crypto/dsa+
@@ -354,19 +253,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/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+
@@ -380,5 +279,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+

16
cmd/tailscaled/main.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"os"
"strings"
)
func main() {
if strings.HasSuffix(os.Args[0], "tailscaled") {
tailscaled_main()
} else if strings.HasSuffix(os.Args[0], "tailscale") {
tailscale_main()
} else {
panic(os.Args[0])
}
}

View File

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

View File

@@ -4,7 +4,7 @@
// The tailscale command is the Tailscale command-line client. It interacts
// with the tailscaled node agent.
package main // import "tailscale.com/cmd/tailscale"
package main // import "tailscale.com/cmd/tailscaled"
import (
"fmt"
@@ -12,10 +12,10 @@ import (
"path/filepath"
"strings"
"tailscale.com/cmd/tailscale/cli"
"tailscale.com/cmd/tailscaled/cli"
)
func main() {
func tailscale_main() {
args := os.Args[1:]
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
args = []string{"web", "-cgi"}

View File

@@ -20,29 +20,26 @@ import (
"net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"syscall"
"time"
"tailscale.com/ipn"
"github.com/go-multierror/multierror"
"inet.af/netaddr"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5/tssocks"
"tailscale.com/net/socks5"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/types/netmap"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -52,6 +49,15 @@ import (
"tailscale.com/wgengine/router"
)
// globalStateKey is the ipn.StateKey that tailscaled loads on
// startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
const globalStateKey = "_daemon"
// defaultTunName returns the default tun device name for the platform.
func defaultTunName() string {
switch runtime.GOOS {
@@ -74,27 +80,19 @@ func defaultTunName() string {
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
// or comma-separated list thereof.
tunname string
cleanup bool
debug string
port uint16
statepath string
statedir string
socketpath string
birdSocketPath string
verbose int
socksAddr string // listen address for SOCKS5 server
httpProxyAddr string // listen address for HTTP proxy server
cleanup bool
debug string
tunname string // tun name, "userspace-networking", or comma-separated list thereof
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{
@@ -103,7 +101,7 @@ var subCommands = map[string]*func([]string) error{
"debug": &debugModeFunc,
}
func main() {
func tailscaled_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,
@@ -117,13 +115,10 @@ func main() {
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. 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 {
@@ -155,7 +150,7 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
}
@@ -165,11 +160,6 @@ 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)
@@ -181,76 +171,10 @@ func main() {
}
}
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 := 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
}
}
switch goos {
default:
o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey
case "windows":
// Not those.
}
return o
}
func run() error {
var err error
pol := logpolicy.New(logtail.CollectionNode)
pol := logpolicy.New("tailnode.log.tailscale.io")
pol.SetVerbosityLevel(args.verbose)
defer func() {
// Finish uploading logs after closing everything else.
@@ -276,19 +200,13 @@ func run() error {
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
if os.Getenv("TS_PLEASE_PANIC") != "" {
panic("TS_PLEASE_PANIC asked us to panic")
}
dns.Cleanup(logf, args.tunname)
router.Cleanup(logf, args.tunname)
return nil
}
if args.statepath == "" && args.statedir == "" {
log.Fatalf("--statedir (or at least --state) is required")
}
if err := trySynologyMigration(statePathOrDefault()); err != nil {
log.Printf("error in synology migration: %v", err)
if args.statepath == "" {
log.Fatalf("--state is required")
}
var debugMux *http.ServeMux
@@ -303,40 +221,60 @@ func run() error {
}
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)
}
}
e, useNetstack, err := createEngine(logf, linkMon)
if err != nil {
logf("wgengine.New: %v", err)
return err
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
var ns *netstack.Impl
if useNetstack || wrapNetstack {
onlySubnets := wrapNetstack && !useNetstack
ns = mustStartNetstack(logf, e, onlySubnets)
}
ns, err := newNetstack(logf, e)
if err != nil {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
if socksListener != nil || httpProxyListener != nil {
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
if socksListener != nil {
srv := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
}
if socksListener != nil {
go func() {
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
}()
var (
mu sync.Mutex // guards the following field
dns netstack.DNSMap
)
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
mu.Lock()
defer mu.Unlock()
dns = netstack.DNSMapFromNetworkMap(nm)
})
useNetstackForIP := func(ip netaddr.IP) bool {
// TODO(bradfitz): this isn't exactly right.
// We should also support subnets when the
// prefs are configured as such.
return tsaddr.IsTailscaleIP(ip)
}
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := dns.Resolve(ctx, addr)
if err != nil {
return nil, err
}
if ns != nil && useNetstackForIP(ipp.IP()) {
return ns.DialContextTCP(ctx, addr)
}
var d net.Dialer
return d.DialContext(ctx, network, ipp.String())
}
go func() {
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
}()
}
e = wgengine.NewWatchdog(e)
@@ -360,29 +298,15 @@ func run() error {
}
}()
opts := ipnServerOpts()
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
logf("ipnserver.StateStore: %v", err)
return err
opts := ipnserver.Options{
SocketPath: args.socketpath,
Port: 41112,
StatePath: args.statepath,
AutostartStateKey: globalStateKey,
SurviveDisconnects: runtime.GOOS != "windows",
DebugMux: debugMux,
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
if err != nil {
logf("ipnserver.New: %v", err)
return err
}
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = srv.Run(ctx, ln)
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
logf("ipnserver.Run: %v", err)
@@ -406,7 +330,7 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, us
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()
@@ -423,9 +347,9 @@ func shouldWrapNetstack() bool {
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
@@ -436,38 +360,22 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
ListenPort: args.port,
LinkMonitor: linkMon,
}
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 {
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:") {
conf.IsTAP = true
e, err := wgengine.NewUserspaceEngine(logf, conf)
return e, false, err
}
r, err := router.New(logf, dev, linkMon)
r, err := router.New(logf, dev)
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
@@ -484,7 +392,6 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
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)
@@ -493,11 +400,6 @@ func newDebugMux() *http.ServeMux {
return mux
}
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
func runDebugServer(mux *http.ServeMux, addr string) {
srv := &http.Server{
Addr: addr,
@@ -508,54 +410,17 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func newNetstack(logf logger.Logf, 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)
}
// 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
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
}

View File

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

View File

@@ -2,7 +2,7 @@
Description=Tailscale node agent
Documentation=https://tailscale.com/kb/
Wants=network-pre.target
After=network-pre.target NetworkManager.service systemd-resolved.service
After=network-pre.target
[Service]
EnvironmentFile=/etc/default/tailscaled
@@ -15,7 +15,7 @@ Restart=on-failure
RuntimeDirectory=tailscale
RuntimeDirectoryMode=0755
StateDirectory=tailscale
StateDirectoryMode=0700
StateDirectoryMode=0750
CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify

View File

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

View File

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

View File

@@ -19,25 +19,22 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tstun"
"tailscale.com/safesocket"
"tailscale.com/tempfork/wireguard-windows/firewall"
"tailscale.com/types/logger"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
@@ -65,24 +62,15 @@ type ipnService struct {
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
svcAccepts := svc.AcceptStop
if winutil.GetRegInteger("FlushDNSOnSessionUnlock", 0) != 0 {
svcAccepts |= svc.AcceptSessionChange
}
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
args := []string{"/subproc", service.Policy.PublicID.String()}
// Make a logger without a date prefix, as filelogger
// and logtail both already add their own. All we really want
// from the log package is the automatic newline.
logger := log.New(os.Stderr, "", 0)
ipnserver.BabysitProc(ctx, args, logger.Printf)
ipnserver.BabysitProc(ctx, args, log.Printf)
}()
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
for ctx.Err() == nil {
select {
@@ -93,9 +81,6 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
cancel()
case svc.Interrogate:
changes <- cmd.CurrentStatus
case svc.SessionChange:
handleSessionChange(cmd)
changes <- cmd.CurrentStatus
}
}
}
@@ -142,6 +127,16 @@ func beFirewallKillswitch() bool {
log.SetFlags(0)
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
}
}()
guid, err := windows.GUIDFromString(os.Args[2])
if err != nil {
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
@@ -149,29 +144,17 @@ func beFirewallKillswitch() bool {
luid, err := winipcfg.LUIDFromGUID(&guid)
if err != nil {
log.Fatalf("no interface with GUID %q: %v", guid, err)
log.Fatalf("no interface with GUID %q", guid)
}
noProtection := false
var dnsIPs []net.IP // unused in called code.
start := time.Now()
fw, err := wf.New(uint64(luid))
if err != nil {
log.Fatalf("failed to enable firewall: %v", err)
}
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
log.Printf("killswitch enabled, took %s", time.Since(start))
// Note(maisem): when local lan access toggled, tailscaled needs to
// inform the firewall to let local routes through. The set of routes
// is passed in via stdin encoded in json.
dcd := json.NewDecoder(os.Stdin)
for {
var routes []netaddr.IPPrefix
if err := dcd.Decode(&routes); err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
if err := fw.UpdatePermittedRoutes(routes); err != nil {
log.Fatalf("failed to update routes (%v)", err)
}
}
// Block until the monitor goroutine shuts us down.
select {}
}
func startIPNServer(ctx context.Context, logid string) error {
@@ -182,7 +165,7 @@ func startIPNServer(ctx context.Context, logid string) error {
if err != nil {
return nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev, nil)
r, err := router.New(logf, dev)
if err != nil {
dev.Close()
return nil, fmt.Errorf("router: %w", err)
@@ -207,14 +190,9 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, fmt.Errorf("engine: %w", err)
}
ns, err := newNetstack(logf, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
if err := ns.Start(); err != nil {
return nil, fmt.Errorf("failed to start netstack: %w", err)
onlySubnets := true
if wrapNetstack {
mustStartNetstack(logf, eng, onlySubnets)
}
return wgengine.NewWatchdog(eng), nil
}
@@ -252,6 +230,12 @@ func startIPNServer(ctx context.Context, logid string) error {
}
}()
opts := ipnserver.Options{
Port: 41112,
SurviveDisconnects: false,
StatePath: args.statepath,
}
// getEngine is called by ipnserver to get the engine. It's
// not called concurrently and is not called again once it
// successfully returns an engine.
@@ -276,38 +260,13 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
}
}
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
return err
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
err := ipnserver.Run(ctx, logf, logid, getEngine, opts)
if err != nil {
logf("ipnserver.Run: %v", err)
}
return err
}
func handleSessionChange(chgRequest svc.ChangeRequest) {
if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
return
}
log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
go func() {
err := dns.Flush()
if err != nil {
log.Printf("Error flushing DNS on session unlock: %v", err)
}
}()
}
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
getTickCount64Proc = kernel32.NewProc("GetTickCount64")

View File

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

View File

@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
// The tsshd binary is an SSH server that accepts connections
@@ -30,8 +29,8 @@ import (
"time"
"unsafe"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
@@ -157,9 +156,8 @@ func handleSSH(s ssh.Session) {
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
} else {
s.Exit(0)
}
s.Exit(0)
return
}

View File

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

View File

@@ -14,11 +14,11 @@ import (
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
)
type LoginGoal struct {
@@ -281,6 +281,7 @@ func (c *Auto) authRoutine() {
report := func(err error, msg string) {
c.logf("[v1] %s: %v", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
@@ -339,9 +340,11 @@ func (c *Auto) authRoutine() {
continue
}
if url != "" {
// goal.url ought to be empty here.
// However, not all control servers get this right,
// and logging about it here just generates noise.
if goal.url != "" {
err = fmt.Errorf("[unexpected] server required a new URL?")
report(err, "WaitLoginURL")
}
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: true,
@@ -428,7 +431,7 @@ func (c *Auto) mapRoutine() {
report := func(err error, msg string) {
c.logf("[v1] %s: %v", msg, err)
err = fmt.Errorf("%s: %w", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
@@ -573,12 +576,9 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
c.logf("[v1] sendStatus: %s: %v", who, state)
var p *persist.Persist
var loginFin, logoutFin *empty.Message
var fin *empty.Message
if state == StateAuthenticated {
loginFin = new(empty.Message)
}
if state == StateNotAuthenticated {
logoutFin = new(empty.Message)
fin = new(empty.Message)
}
if nm != nil && loggedIn && synced {
pp := c.direct.GetPersist()
@@ -589,14 +589,15 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
nm = nil
}
new := Status{
LoginFinished: loginFin,
LogoutFinished: logoutFin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
Err: err,
LoginFinished: fin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
}
if err != nil {
new.Err = err.Error()
}
if statusFunc != nil {
statusFunc(new)
@@ -697,7 +698,7 @@ func (c *Auto) Shutdown() {
// NodePublicKey returns the node public key currently in use. This is
// used exclusively in tests.
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
priv := c.direct.GetPersist()
return priv.PrivateNodeKey.Public()
}
@@ -711,9 +712,3 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) {
func (c *Auto) TestOnlyTimeNow() time.Time {
return c.timeNow()
}
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
return c.direct.SetDNS(ctx, req)
}

View File

@@ -20,7 +20,6 @@ type LoginFlags int
const (
LoginDefault = LoginFlags(0)
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
LoginEphemeral // set RegisterRequest.Ephemeral
)
// Client represents a client connection to the control server.
@@ -70,18 +69,9 @@ type Client interface {
SetNetInfo(*tailcfg.NetInfo)
// UpdateEndpoints changes the Endpoint structure that will be sent
// in subsequent node registration requests.
// The localPort field is unused except for integration tests in another repo.
// TODO: localPort seems to be obsolete, remove it.
// TODO: a server-side change would let us simply upload this
// in a separate http request. It has nothing to do with the rest of
// the state machine.
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
}
// UserVisibleError is an error that should be shown to users.
type UserVisibleError string
func (e UserVisibleError) Error() string { return string(e) }
func (e UserVisibleError) UserVisibleError() string { return string(e) }

View File

@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestStatusEqual(t *testing.T) {
// Verify that the Equal method stays in sync with reality
equalHandles := []string{"LoginFinished", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
equalHandles := []string{"LoginFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, equalHandles)
@@ -75,3 +75,10 @@ func TestStatusEqual(t *testing.T) {
}
}
}
func TestOSVersion(t *testing.T) {
if osVersion == nil {
t.Skip("not available for OS")
}
t.Logf("Got: %#q", osVersion())
}

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"log"
"net/http"
"regexp"
"runtime"
"strconv"
"time"
@@ -41,82 +42,28 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
}
}
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func scrubbedGoroutineDump() []byte {
var buf []byte
// Grab stacks multiple times into increasingly larger buffer sizes
// to minimize the risk that we blow past our iOS memory limit.
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
// It fit.
break
}
}
return scrubHex(buf)
}
buf := make([]byte, 1<<20)
buf = buf[:runtime.Stack(buf, true)]
func scrubHex(buf []byte) []byte {
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
foreachHexAddress(buf, func(in []byte) {
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
if string(in) == "0x0" {
return
return in
}
if v, ok := saw[string(in)]; ok {
for i := range in {
in[i] = '_'
}
copy(in, v)
return
return v
}
inStr := string(in)
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
for i := range in {
in[i] = '_'
}
if err != nil {
in[0] = '?'
return
return []byte("??")
}
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
saw[inStr] = v
copy(in, v)
saw[string(in)] = v
return v
})
return buf
}
var ohx = []byte("0x")
// foreachHexAddress calls f with each subslice of b that matches
// regexp `0x[0-9a-f]*`.
func foreachHexAddress(b []byte, f func([]byte)) {
for len(b) > 0 {
i := bytes.Index(b, ohx)
if i == -1 {
return
}
b = b[i:]
hx := hexPrefix(b)
f(hx)
b = b[len(hx):]
}
}
func hexPrefix(b []byte) []byte {
for i, c := range b {
if i < 2 {
continue
}
if !isHexByte(c) {
return b[:i]
}
}
return b
}
func isHexByte(b byte) bool {
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}

View File

@@ -9,22 +9,3 @@ import "testing"
func TestScrubbedGoroutineDump(t *testing.T) {
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
}
func TestScrubHex(t *testing.T) {
tests := []struct {
in, want string
}{
{"foo", "foo"},
{"", ""},
{"0x", "?_"},
{"0x001 and same 0x001", "v1%1_ and same v1%1_"},
{"0x008 and same 0x008", "v1%0_ and same v1%0_"},
{"0x001 and diff 0x002", "v1%1_ and diff v2%2_"},
}
for _, tt := range tests {
got := scrubHex([]byte(tt.in))
if string(got) != tt.want {
t.Errorf("for input:\n%s\n\ngot:\n%s\n\nwant:\n%s\n", tt.in, got, tt.want)
}
}
}

View File

@@ -7,6 +7,7 @@ package controlclient
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
@@ -19,6 +20,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
@@ -27,12 +29,9 @@ import (
"sync/atomic"
"time"
"go4.org/mem"
"golang.org/x/crypto/nacl/box"
"inet.af/netaddr"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
@@ -41,13 +40,14 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/monitor"
)
@@ -61,35 +61,33 @@ type Direct struct {
keepAlive bool
logf logger.Logf
linkMon *monitor.Mon // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
discoPubKey tailcfg.DiscoKey
getMachinePrivKey func() (wgkey.Private, error)
debugFlags []string
keepSharerAndUserSplit bool
skipIPForwardingCheck bool
pinger Pinger
mu sync.Mutex // mutex guards the following fields
serverKey key.MachinePublic
serverKey wgkey.Key
persist persist.Persist
authKey string
tryingNewKey key.NodePrivate
tryingNewKey wgkey.Private
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []tailcfg.Endpoint
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
lastPingURL string // last PingRequest.URL received, for dup suppression
}
type Options struct {
Persist persist.Persist // initial persistent data
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic
Persist persist.Persist // initial persistent data
GetMachinePrivateKey func() (wgkey.Private, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
@@ -105,18 +103,6 @@ type Options struct {
// forwarding works and should not be double-checked by the
// controlclient package.
SkipIPForwardingCheck bool
// Pinger optionally specifies the Pinger to use to satisfy
// MapResponse.PingRequest queries from the control plane.
// If nil, PingRequest queries are not answered.
Pinger Pinger
}
// Pinger is a subset of the wgengine.Engine interface, containing just the Ping method.
type Pinger interface {
// Ping is a request to start a discovery or TSMP ping with the peer handling
// the given IP and then call cb with its ping latency & method.
Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult))
}
type Decompressor interface {
@@ -147,20 +133,13 @@ func NewDirect(opts Options) (*Direct, error) {
}
httpc := opts.HTTPTestClient
if httpc == nil && runtime.GOOS == "js" {
// In js/wasm, net/http.Transport (as of Go 1.18) will
// only use the browser's Fetch API if you're using
// the DefaultClient (or a client without dial hooks
// etc set).
httpc = http.DefaultClient
}
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
}
dialer := netns.NewDialer(opts.Logf)
dialer := netns.NewDialer()
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
@@ -186,16 +165,48 @@ func NewDirect(opts Options) (*Direct, error) {
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
}
if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New())
c.SetHostinfo(NewHostinfo())
} else {
c.SetHostinfo(opts.Hostinfo)
}
return c, nil
}
var osVersion func() string // non-nil on some platforms
func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
var osv string
if osVersion != nil {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.Long,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
Package: packageType(),
GoArch: runtime.GOARCH,
}
}
func packageType() string {
switch runtime.GOOS {
case "windows":
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
case "darwin":
// Using tailscaled or IPNExtension?
exe, _ := os.Executable()
return filepath.Base(exe)
}
return ""
}
// SetHostinfo clones the provided Hostinfo and remembers it for the
// next update. It reports whether the Hostinfo has changed.
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
@@ -292,8 +303,8 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
tryingNewKey := c.tryingNewKey
serverKey := c.serverKey
authKey := c.authKey
hi := c.hostinfo.Clone()
backendLogID := hi.BackendLogID
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
@@ -327,14 +338,13 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if err != nil {
return regen, opt.URL, err
}
c.logf("control server key %s from %s", serverKey.ShortString(), c.serverURL)
c.mu.Lock()
c.serverKey = serverKey
c.mu.Unlock()
}
var oldNodeKey key.NodePublic
var oldNodeKey wgkey.Key
switch {
case opt.Logout:
tryingNewKey = persist.PrivateNodeKey
@@ -343,7 +353,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
case regen || persist.PrivateNodeKey.IsZero():
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
tryingNewKey = key.NewNode()
key, err := wgkey.NewPrivate()
if err != nil {
c.logf("login keygen: %v", err)
return regen, opt.URL, err
}
tryingNewKey = key
default:
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
@@ -365,12 +380,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
OldNodeKey: oldNodeKey,
NodeKey: tryingNewKey.Public(),
Hostinfo: hi,
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
Hostinfo: hostinfo,
Followup: opt.URL,
Timestamp: &now,
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
}
if opt.Logout {
request.Expiry = time.Unix(123, 0) // far in the past
@@ -401,13 +415,13 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterRequest: %s", j)
}
bodyData, err := encode(request, serverKey, machinePrivKey)
bodyData, err := encode(request, &serverKey, &machinePrivKey)
if err != nil {
return regen, opt.URL, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().UntypedHexString())
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, opt.URL, err
@@ -425,7 +439,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
res.StatusCode, strings.TrimSpace(string(msg)))
}
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, serverKey, machinePrivKey); err != nil {
if err := decode(res, &resp, &serverKey, &machinePrivKey); err != nil {
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
@@ -438,9 +452,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
if resp.Error != "" {
return false, "", UserVisibleError(resp.Error)
}
if resp.NodeKeyExpired {
if regen {
return true, "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
@@ -559,21 +570,12 @@ const pollTimeout = 120 * time.Second
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
metricMapRequests.Add(1)
metricMapRequestsActive.Add(1)
defer metricMapRequestsActive.Add(-1)
if maxPolls == -1 {
metricMapRequestsPoll.Add(1)
} else {
metricMapRequestsLite.Add(1)
}
c.mu.Lock()
persist := c.persist
serverURL := c.serverURL
serverKey := c.serverKey
hi := c.hostinfo.Clone()
backendLogID := hi.BackendLogID
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
localPort := c.localPort
var epStrs []string
var epTypes []tailcfg.EndpointType
@@ -612,18 +614,18 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
request := &tailcfg.MapRequest{
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: persist.PrivateNodeKey.Public(),
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: epStrs,
EndpointTypes: epTypes,
Stream: allowStream,
Hostinfo: hi,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
@@ -632,9 +634,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
if health.NetworkCategoryHealth() != nil {
extraDebugFlags = append(extraDebugFlags, "warn-network-category-unhealthy")
}
if hostinfo.DisabledEtcAptSource() {
extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled")
}
if len(extraDebugFlags) > 0 {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
@@ -654,7 +653,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
request.ReadOnly = true
}
bodyData, err := encode(request, serverKey, machinePrivKey)
bodyData, err := encode(request, &serverKey, &machinePrivKey)
if err != nil {
vlogf("netmap: encode: %v", err)
return err
@@ -663,9 +662,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
ctx, cancel := context.WithCancel(ctx)
defer cancel()
machinePubKey := machinePrivKey.Public()
machinePubKey := tailcfg.MachineKey(machinePrivKey.Public())
t0 := time.Now()
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.UntypedHexString())
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
if err != nil {
@@ -752,19 +751,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
var resp tailcfg.MapResponse
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
if err := c.decodeMsg(msg, &resp, &machinePrivKey); err != nil {
vlogf("netmap: decode error: %v")
return err
}
metricMapResponseMessages.Add(1)
if allowStream {
health.GotStreamedMapResponse()
}
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
metricMapResponsePings.Add(1)
if pr := resp.PingRequest; pr != nil {
go answerPing(c.logf, c.httpc, pr)
}
@@ -781,23 +777,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return ctx.Err()
}
if resp.KeepAlive {
metricMapResponseKeepAlives.Add(1)
continue
}
metricMapResponseMap.Add(1)
if i > 0 {
metricMapResponseMapDelta.Add(1)
}
hasDebug := resp.Debug != nil
// being conservative here, if Debug not present set to False
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
if hasDebug {
if code := resp.Debug.Exit; code != nil {
c.logf("exiting process with status %v per controlplane", *code)
os.Exit(*code)
}
if resp.Debug != nil {
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
@@ -819,6 +802,28 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return errors.New("MapResponse lacked node")
}
// Temporarily (2020-06-29) support removing all but
// discovery-supporting nodes during development, for
// less noise.
if Debug.OnlyDisco {
anyOld, numDisco := false, 0
for _, p := range nm.Peers {
if p.DiscoKey.IsZero() {
anyOld = true
} else {
numDisco++
}
}
if anyOld {
filtered := make([]*tailcfg.Node, 0, numDisco)
for _, p := range nm.Peers {
if !p.DiscoKey.IsZero() {
filtered = append(filtered, p)
}
}
nm.Peers = filtered
}
}
if Debug.StripEndpoints {
for _, p := range resp.Peers {
// We need at least one endpoint here for now else
@@ -832,21 +837,22 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
// Get latest localPort. This might've changed if
// a lite map update occurred meanwhile. This only affects
// a lite map update occured meanwhile. This only affects
// the end-to-end test.
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
c.mu.Lock()
nm.LocalPort = c.localPort
c.mu.Unlock()
// Occasionally print the netmap header.
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
// Printing the netmap can be extremely verbose, but is very
// handy for debugging. Let's limit how often we do it.
// Code elsewhere prints netmap diffs every time, so this
// occasional full dump, plus incremental diffs, should do
// the job.
now := c.timeNow()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
}
c.mu.Lock()
@@ -861,7 +867,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return nil
}
func decode(res *http.Response, v interface{}, serverKey key.MachinePublic, mkey key.MachinePrivate) error {
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
defer res.Body.Close()
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
@@ -880,14 +886,14 @@ var (
var jsonEscapedZero = []byte(`\u0000`)
func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey key.MachinePrivate) error {
func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey *wgkey.Private) error {
c.mu.Lock()
serverKey := c.serverKey
c.mu.Unlock()
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
if !ok {
return errors.New("cannot decrypt response")
decrypted, err := decryptMsg(msg, &serverKey, machinePrivKey)
if err != nil {
return err
}
var b []byte
if c.newDecompressor == nil {
@@ -919,10 +925,10 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey key.Machine
}
func decodeMsg(msg []byte, v interface{}, serverKey key.MachinePublic, machinePrivKey key.MachinePrivate) error {
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
if !ok {
return errors.New("cannot decrypt response")
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, machinePrivKey *wgkey.Private) error {
decrypted, err := decryptMsg(msg, serverKey, machinePrivKey)
if err != nil {
return err
}
if bytes.Contains(decrypted, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
@@ -933,7 +939,23 @@ func decodeMsg(msg []byte, v interface{}, serverKey key.MachinePublic, machinePr
return nil
}
func encode(v interface{}, serverKey key.MachinePublic, mkey key.MachinePrivate) ([]byte, error) {
func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
var nonce [24]byte
if len(msg) < len(nonce)+1 {
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
}
copy(nonce[:], msg)
msg = msg[len(nonce):]
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
if !ok {
return nil, fmt.Errorf("cannot decrypt response (len %d + nonce %d = %d)", len(msg), len(nonce), len(msg)+len(nonce))
}
return decrypted, nil
}
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
@@ -943,32 +965,38 @@ func encode(v interface{}, serverKey key.MachinePublic, mkey key.MachinePrivate)
log.Printf("MapRequest: %s", b)
}
}
return mkey.SealTo(serverKey, b), nil
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
msg := box.Seal(nonce[:], b, &nonce, pub, pri)
return msg, nil
}
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (key.MachinePublic, error) {
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
req, err := http.NewRequest("GET", serverURL+"/key", nil)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("create control key request: %v", err)
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
}
req = req.WithContext(ctx)
res, err := httpc.Do(req)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("fetch control key: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<16))
if err != nil {
return key.MachinePublic{}, fmt.Errorf("fetch control key response: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
}
if res.StatusCode != 200 {
return key.MachinePublic{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
}
k, err := key.ParseMachinePublicUntyped(mem.B(b))
key, err := wgkey.ParseHex(string(b))
if err != nil {
return key.MachinePublic{}, fmt.Errorf("fetch control key: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
}
return k, nil
return key, nil
}
// Debug contains temporary internal-only debug knobs.
@@ -978,18 +1006,21 @@ var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
}
func initDebug() debug {
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
Disco: os.Getenv("TS_DEBUG_USE_DISCO") == "" || envBool("TS_DEBUG_USE_DISCO"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
}
@@ -1124,23 +1155,6 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
return false
}
// isUniquePingRequest reports whether pr contains a new PingRequest.URL
// not already handled, noting its value when returning true.
func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
if pr == nil || pr.URL == "" {
// Bogus.
return false
}
c.mu.Lock()
defer c.mu.Unlock()
if pr.URL == c.lastPingURL {
return false
}
c.lastPingURL = pr.URL
return true
}
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
if pr.URL == "" {
logf("invalid PingRequest with no URL")
@@ -1197,129 +1211,3 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
}
}
}
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
metricSetDNS.Add(1)
defer func() {
if err != nil {
metricSetDNSError.Add(1)
}
}()
c.mu.Lock()
serverKey := c.serverKey
c.mu.Unlock()
if serverKey.IsZero() {
return errors.New("zero serverKey")
}
machinePrivKey, err := c.getMachinePrivKey()
if err != nil {
return fmt.Errorf("getMachinePrivKey: %w", err)
}
if machinePrivKey.IsZero() {
return errors.New("getMachinePrivKey returned zero key")
}
bodyData, err := encode(req, serverKey, machinePrivKey)
if err != nil {
return err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().UntypedHexString())
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
if err != nil {
return err
}
res, err := c.httpc.Do(hreq)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes struct{} // no fields yet
if err := decode(res, &setDNSRes, serverKey, machinePrivKey); err != nil {
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return fmt.Errorf("set-dns-response: %v", err)
}
return nil
}
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
// with ping response data.
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
var err error
if pr.URL == "" {
return errors.New("invalid PingRequest with no URL")
}
if pr.IP.IsZero() {
return errors.New("PingRequest without IP")
}
if !strings.Contains(pr.Types, "TSMP") {
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
}
now := time.Now()
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
// Currently does not check for error since we just return if it fails.
err = postPingResult(now, logf, c, pr, res)
})
return err
}
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
if res.Err != "" {
return errors.New(res.Err)
}
duration := time.Since(now)
if pr.Log {
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
jsonPingRes, err := json.Marshal(res)
if err != nil {
return err
}
// Send the results of the Ping, back to control URL.
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
if err != nil {
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
}
if pr.Log {
logf("tsmpPing: sending ping results to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
}
return nil
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
metricMapRequests = clientmetric.NewCounter("controlclient_map_requests")
metricMapRequestsLite = clientmetric.NewCounter("controlclient_map_requests_lite")
metricMapRequestsPoll = clientmetric.NewCounter("controlclient_map_requests_poll")
metricMapResponseMessages = clientmetric.NewCounter("controlclient_map_response_message") // any message type
metricMapResponsePings = clientmetric.NewCounter("controlclient_map_response_ping")
metricMapResponseKeepAlives = clientmetric.NewCounter("controlclient_map_response_keepalive")
metricMapResponseMap = clientmetric.NewCounter("controlclient_map_response_map") // any non-keepalive map response
metricMapResponseMapDelta = clientmetric.NewCounter("controlclient_map_response_map_delta") // 2nd+ non-keepalive map response
metricSetDNS = clientmetric.NewCounter("controlclient_setdns")
metricSetDNSError = clientmetric.NewCounter("controlclient_setdns_error")
)

View File

@@ -6,29 +6,27 @@ package controlclient
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
)
func TestNewDirect(t *testing.T) {
hi := hostinfo.New()
hi := NewHostinfo()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
k := key.NewMachine()
key, err := wgkey.NewPrivate()
if err != nil {
t.Error(err)
}
opts := Options{
ServerURL: "https://example.com",
Hostinfo: hi,
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
GetMachinePrivateKey: func() (wgkey.Private, error) {
return key, nil
},
}
c, err := NewDirect(opts)
@@ -58,7 +56,7 @@ func TestNewDirect(t *testing.T) {
if changed {
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
}
hi = hostinfo.New()
hi = NewHostinfo()
hi.Hostname = "different host name"
changed = c.SetHostinfo(hi)
if !changed {
@@ -94,52 +92,14 @@ func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
return
}
func TestTsmpPing(t *testing.T) {
hi := hostinfo.New()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
k := key.NewMachine()
opts := Options{
ServerURL: "https://example.com",
Hostinfo: hi,
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
},
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
}
c, err := NewDirect(opts)
if err != nil {
t.Fatal(err)
}
pingRes := &ipnstate.PingResult{
IP: "123.456.7890",
Err: "",
NodeName: "testnode",
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body := new(ipnstate.PingResult)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if pingRes.IP != body.IP {
t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP)
}
w.WriteHeader(200)
}))
defer ts.Close()
now := time.Now()
pr := &tailcfg.PingRequest{
URL: ts.URL,
}
err = postPingResult(now, t.Logf, c.httpc, pr, pingRes)
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -2,48 +2,26 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !android
// +build linux,!android
package hostinfo
package controlclient
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"syscall"
"go4.org/mem"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
func init() {
osVersion = osVersionLinux
if v := linuxDeviceModel(); v != "" {
SetDeviceModel(v)
}
}
func linuxDeviceModel() string {
for _, path := range []string{
// First try the Synology-specific location.
// Example: "DS916+-j"
"/proc/sys/kernel/syno_hw_version",
// Otherwise, try the Devicetree model, usually set on
// ARM SBCs, etc.
// Example: "Raspberry Pi 4 Model B Rev 1.2"
"/sys/firmware/devicetree/base/model", // Raspberry Pi 4 Model B Rev 1.2"
} {
b, _ := os.ReadFile(path)
if s := strings.Trim(string(b), "\x00\r\n\t "); s != "" {
return s
}
}
return ""
}
func osVersionLinux() string {
@@ -81,8 +59,8 @@ func osVersionLinux() string {
if inContainer() {
attrBuf.WriteString("; container")
}
if env := GetEnvType(); env != "" {
fmt.Fprintf(&attrBuf, "; env=%s", env)
if inKnative() {
attrBuf.WriteString("; env=kn")
}
attr := attrBuf.String()
@@ -115,3 +93,31 @@ func osVersionLinux() string {
}
return fmt.Sprintf("Other%s", attr)
}
func inContainer() (ret bool) {
lineread.File("/proc/1/cgroup", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret = true
return io.EOF // arbitrary non-nil error to stop loop
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
ret = true
return io.EOF
}
return nil
})
return
}
func inKnative() bool {
// https://cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
return true
}
return false
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
import (
"os/exec"
"strings"
"sync/atomic"
"syscall"
)
func init() {
osVersion = osVersionWindows
}
var winVerCache atomic.Value // of string
func osVersionWindows() string {
if s, ok := winVerCache.Load().(string); ok {
return s
}
cmd := exec.Command("cmd", "/c", "ver")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
s := strings.TrimSpace(string(out))
s = strings.TrimPrefix(s, "Microsoft Windows [")
s = strings.TrimSuffix(s, "]")
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
if sp := strings.Index(s, " "); sp != -1 {
s = s[sp+1:]
}
if s != "" {
winVerCache.Store(s)
}
return s // "10.0.19041.388", ideally
}

View File

@@ -6,15 +6,12 @@ package controlclient
import (
"log"
"os"
"sort"
"strconv"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/filter"
)
@@ -28,10 +25,10 @@ import (
// one MapRequest).
type mapSession struct {
// Immutable fields.
privateNodeKey key.NodePrivate
privateNodeKey wgkey.Private
logf logger.Logf
vlogf logger.Logf
machinePubKey key.MachinePublic
machinePubKey tailcfg.MachineKey
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
// Fields storing state over the the coards of multiple MapResponses.
@@ -43,14 +40,13 @@ type mapSession struct {
collectServices bool
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string
lastHealth []string
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.
netMapBuilding *netmap.NetworkMap
}
func newMapSession(privateNodeKey key.NodePrivate) *mapSession {
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
ms := &mapSession{
privateNodeKey: privateNodeKey,
logf: logger.Discard,
@@ -105,12 +101,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.Domain != "" {
ms.lastDomain = resp.Domain
}
if resp.Health != nil {
ms.lastHealth = resp.Health
}
nm := &netmap.NetworkMap{
NodeKey: ms.privateNodeKey.Public(),
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
@@ -121,7 +114,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: resp.Debug,
ControlHealth: ms.lastHealth,
}
ms.netMapBuilding = nm
@@ -132,7 +124,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
nm.SelfNode = node
nm.Expiry = node.KeyExpiry
nm.Name = node.Name
nm.Addresses = filterSelfAddresses(node.Addresses)
nm.Addresses = node.Addresses
nm.User = node.User
nm.Hostinfo = node.Hostinfo
if node.MachineAuthorized {
@@ -288,19 +280,3 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
}
return v2
}
var debugSelfIPv6Only, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_SELF_V6_ONLY"))
func filterSelfAddresses(in []netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
switch {
default:
return in
case debugSelfIPv6Only:
for _, a := range in {
if a.IP().Is6() {
ret = append(ret, a)
}
}
return ret
}
}

View File

@@ -13,8 +13,8 @@ import (
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/wgkey"
)
func TestUndeltaPeers(t *testing.T) {
@@ -170,7 +170,11 @@ func formatNodes(nodes []*tailcfg.Node) string {
}
func newTestMapSession(t *testing.T) *mapSession {
return newMapSession(key.NewNode())
k, err := wgkey.NewPrivate()
if err != nil {
t.Fatal(err)
}
return newMapSession(k)
}
func TestNetmapForResponse(t *testing.T) {
@@ -214,7 +218,7 @@ func TestNetmapForResponse(t *testing.T) {
}
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
DNSConfig: nil, // implicit
DNSConfig: nil, // implict
})
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
t.Fatalf("2nd DNS wrong")

View File

@@ -10,34 +10,22 @@ import (
"fmt"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
)
var (
errNoCertStore = errors.New("no certificate store")
errCertificateNotConfigured = errors.New("no certificate subject configured")
errUnsupportedSignatureVersion = errors.New("unsupported signature version")
errNoCertStore = errors.New("no certificate store")
errCertificateNotConfigured = errors.New("no certificate subject configured")
)
// HashRegisterRequest generates the hash required sign or verify a
// tailcfg.RegisterRequest.
func HashRegisterRequest(
version tailcfg.SignatureType, ts time.Time, serverURL string, deviceCert []byte,
serverPubKey, machinePubKey key.MachinePublic) ([]byte, error) {
// tailcfg.RegisterRequest with tailcfg.SignatureV1.
func HashRegisterRequest(ts time.Time, serverURL string, deviceCert []byte, serverPubKey, machinePubKey wgkey.Key) []byte {
h := crypto.SHA256.New()
// hash.Hash.Write never returns an error, so we don't check for one here.
switch version {
case tailcfg.SignatureV1:
fmt.Fprintf(h, "%s%s%s%s%s",
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey.ShortString(), machinePubKey.ShortString())
case tailcfg.SignatureV2:
fmt.Fprintf(h, "%s%s%s%s%s",
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
default:
return nil, errUnsupportedSignatureVersion
}
fmt.Fprintf(h, "%s%s%s%s%s",
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
return h.Sum(nil), nil
return h.Sum(nil)
}

View File

@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows && cgo
// +build windows,cgo
// darwin,cgo is also supported by certstore but machineCertificateSubject will
@@ -19,9 +18,9 @@ import (
"fmt"
"sync"
"github.com/tailscale/certstore"
"github.com/github/certstore"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
"tailscale.com/util/winutil"
)
@@ -125,7 +124,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
// using that identity's public key. In addition to the signature, the full
// certificate chain is included so that the control server can validate the
// certificate from a copy of the root CA's certificate.
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) (err error) {
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("signRegisterRequest: %w", err)
@@ -167,11 +166,7 @@ func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverP
req.DeviceCert = append(req.DeviceCert, c.Raw...)
}
req.SignatureType = tailcfg.SignatureV2
h, err := HashRegisterRequest(req.SignatureType, req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
if err != nil {
return fmt.Errorf("hash: %w", err)
}
h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
@@ -180,6 +175,7 @@ func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverP
if err != nil {
return fmt.Errorf("sign: %w", err)
}
req.SignatureType = tailcfg.SignatureV1
return nil
}

View File

@@ -2,17 +2,16 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows || !cgo
// +build !windows !cgo
package controlclient
import (
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
)
// signRegisterRequest on non-supported platforms always returns errNoCertStore.
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error {
return errNoCertStore
}

View File

@@ -64,12 +64,11 @@ func (s State) String() string {
}
type Status struct {
_ structs.Incomparable
LoginFinished *empty.Message // nonempty when login finishes
LogoutFinished *empty.Message // nonempty when logout finishes
Err error
URL string // interactive URL to visit to finish logging in
NetMap *netmap.NetworkMap // server-pushed configuration
_ structs.Incomparable
LoginFinished *empty.Message // nonempty when login finishes
Err string
URL string // interactive URL to visit to finish logging in
NetMap *netmap.NetworkMap // server-pushed configuration
// The internal state should not be exposed outside this
// package, but we have some automated tests elsewhere that need to
@@ -87,7 +86,6 @@ func (s *Status) Equal(s2 *Status) bool {
}
return s != nil && s2 != nil &&
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
(s.LogoutFinished == nil) == (s2.LogoutFinished == nil) &&
s.Err == s2.Err &&
s.URL == s2.URL &&
reflect.DeepEqual(s.Persist, s2.Persist) &&

View File

@@ -1,34 +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 controlknobs contains client options configurable from control which can be turned on
// or off. The ability to turn options on and off is for incrementally adding features in.
package controlknobs
import (
"os"
"strconv"
"tailscale.com/syncs"
)
// disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnP syncs.AtomicBool
func init() {
v, _ := strconv.ParseBool(os.Getenv("TS_DISABLE_UPNP"))
SetDisableUPnP(v)
}
// DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled.
func DisableUPnP() bool {
return disableUPnP.Get()
}
// SetDisableUPnP sets whether control says that UPnP should be
// disabled.
func SetDisableUPnP(v bool) {
disableUPnP.Set(v)
}

View File

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

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