Compare commits
10 Commits
dsnet/admi
...
v1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76c2982d88 | ||
|
|
3d64eef37b | ||
|
|
4f292740b0 | ||
|
|
e1327154bb | ||
|
|
a702921620 | ||
|
|
b9b7fbdd21 | ||
|
|
9446e5c170 | ||
|
|
75cd82791e | ||
|
|
4d5d5f89a3 | ||
|
|
bb058703ee |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,36 @@
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please note, this template is for definite bugs, not requests for
|
||||
support. If you need help with Tailscale, please email
|
||||
support@tailscale.com. We don't provide support via Github issues. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version information:**
|
||||
- Device: [e.g. iPhone X, laptop]
|
||||
- OS: [e.g. Windows, MacOS]
|
||||
- OS version: [e.g. Windows 10, Ubuntu 18.04]
|
||||
- Tailscale version: [e.g. 0.95-0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,6 +2,25 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or
|
||||
features you've considered.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/depaware.yml
vendored
2
.github/workflows/depaware.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
34
.github/workflows/go_generate.yml
vendored
34
.github/workflows/go_generate.yml
vendored
@@ -1,34 +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@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
mkdir gentools
|
||||
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
|
||||
PATH="$PATH:$(pwd)/gentools" go generate ./...
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
48
.github/workflows/linux-race.yml
vendored
48
.github/workflows/linux-race.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Linux race
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests and benchmarks with -race flag on linux
|
||||
run: go test -race -bench=. -benchtime=1x ./...
|
||||
|
||||
- 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'
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/linux32.yml
vendored
4
.github/workflows/linux32.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
|
||||
run: GOARCH=386 go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
20
.github/workflows/staticcheck.yml
vendored
20
.github/workflows/staticcheck.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
@@ -24,23 +24,11 @@ jobs:
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Install staticcheck
|
||||
run: "GOBIN=~/.local/bin go install honnef.co/go/tools/cmd/staticcheck"
|
||||
|
||||
- name: Print staticcheck version
|
||||
run: "staticcheck -version"
|
||||
run: go run honnef.co/go/tools/cmd/staticcheck -version
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
- name: Run staticcheck
|
||||
run: "go run honnef.co/go/tools/cmd/staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
55
.github/workflows/windows-race.yml
vendored
55
.github/workflows/windows-race.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Windows race
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test with -race flag
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: go test -race -bench . -benchtime 1x ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
7
.github/workflows/windows.yml
vendored
7
.github/workflows/windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.15.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -33,10 +33,7 @@ jobs:
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: go test -bench . -benchtime 1x ./...
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
45
.github/workflows/xe-experimental-vm-test.yml
vendored
45
.github/workflows/xe-experimental-vm-test.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: "integration-vms"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "tstest/integration/vms/**"
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
experimental-linux-vm-test:
|
||||
# To set up a new runner, see tstest/integration/vms/runner.nix
|
||||
runs-on: [ self-hosted, linux, vm_integration_test ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download VM Images
|
||||
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m -no-s3
|
||||
env:
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -run-vm-tests
|
||||
env:
|
||||
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'
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,11 +1,12 @@
|
||||
# Binaries for programs and plugins
|
||||
*~
|
||||
*.tmp
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
cmd/relaynode/relaynode
|
||||
cmd/taillogin/taillogin
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
@@ -17,7 +18,3 @@ cmd/tailscaled/tailscaled
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# direnv config, this may be different for other people so it's probably safer
|
||||
# to make this nonspecific.
|
||||
.envrc
|
||||
|
||||
36
Dockerfile
36
Dockerfile
@@ -2,23 +2,6 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
############################################################################
|
||||
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
@@ -38,28 +21,17 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.16-alpine AS build-env
|
||||
FROM golang:1.14-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# see build_docker.sh
|
||||
ARG VERSION_LONG=""
|
||||
ENV VERSION_LONG=$VERSION_LONG
|
||||
ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
|
||||
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/...
|
||||
RUN go install -v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
|
||||
46
LICENSE
46
LICENSE
@@ -1,29 +1,27 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Tailscale Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
8
Makefile
8
Makefile
@@ -12,13 +12,7 @@ depaware:
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
buildwindows:
|
||||
GOOS=windows GOARCH=amd64 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
check: staticcheck vet depaware
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
13
README.md
13
README.md
@@ -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.16) in module mode. It might
|
||||
release candidate builds (currently Go 1.15) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
@@ -63,13 +63,8 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
|
||||
## About Us
|
||||
|
||||
[Tailscale](https://tailscale.com/) is primarily developed by the
|
||||
people at https://github.com/orgs/tailscale/people. For other contributors,
|
||||
see:
|
||||
|
||||
* https://github.com/tailscale/tailscale/graphs/contributors
|
||||
* https://github.com/tailscale/tailscale-android/graphs/contributors
|
||||
|
||||
## Legal
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
|
||||
from Tailscale Inc.
|
||||
You can learn more about us from [our website](https://tailscale.com).
|
||||
|
||||
WireGuard is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.13.0
|
||||
1.2.2
|
||||
|
||||
802
api.md
802
api.md
@@ -1,802 +0,0 @@
|
||||
# Tailscale API
|
||||
|
||||
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://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
|
||||
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-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
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
|
||||
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
|
||||
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
|
||||
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
|
||||
|
||||
To find the deviceID of a particular device, you can use the ["GET /devices"](#getdevices) API call and generate a list of devices on your network.
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
##### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/device/12345
|
||||
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
"100.105.58.116"
|
||||
],
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"name":"user1-device.example.com",
|
||||
"hostname":"User1-Device",
|
||||
"clientVersion":"date.20201107",
|
||||
"updateAvailable":false,
|
||||
"os":"macOS",
|
||||
"created":"2020-11-20T20:56:49Z",
|
||||
"lastSeen":"2020-11-20T16:15:55-05:00",
|
||||
"keyExpiryDisabled":false,
|
||||
"expires":"2021-05-19T20:56:49Z",
|
||||
"authorized":true,
|
||||
"isExternal":false,
|
||||
"machineKey":"mkey:user1-machine-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"blocksIncomingConnections":false,
|
||||
"enabledRoutes":[
|
||||
|
||||
],
|
||||
"advertisedRoutes":[
|
||||
|
||||
],
|
||||
"clientConnectivity": {
|
||||
"endpoints":[
|
||||
"209.195.87.231:59128",
|
||||
"192.168.0.173:59128"
|
||||
],
|
||||
"derp":"",
|
||||
"mappingVariesByDestIP":false,
|
||||
"latency":{
|
||||
"Dallas":{
|
||||
"latencyMs":60.463043
|
||||
},
|
||||
"New York City":{
|
||||
"preferred":true,
|
||||
"latencyMs":31.323811
|
||||
},
|
||||
"San Francisco":{
|
||||
"latencyMs":81.313389
|
||||
}
|
||||
},
|
||||
"clientSupports":{
|
||||
"hairPinning":false,
|
||||
"ipv6":false,
|
||||
"pcp":false,
|
||||
"pmp":false,
|
||||
"udp":true,
|
||||
"upnp":false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
The device must belong to the user's tailnet.
|
||||
Deleting shared/external devices is not supported.
|
||||
Supply the device of interest in the path using its ID.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
DELETE /api/v2/device/12345
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
-u "tskey-yourapikey123:" -v
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
If successful, the response should be empty:
|
||||
```
|
||||
< HTTP/1.1 200 OK
|
||||
...
|
||||
* Connection #0 to host left intact
|
||||
* Closing connection 0
|
||||
```
|
||||
|
||||
If the device is not owned by your tailnet:
|
||||
```
|
||||
< HTTP/1.1 501 Not Implemented
|
||||
...
|
||||
{"message":"cannot delete devices outside of your tailnet"}
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : []
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`routes` - The new list of enabled subnet routes in JSON.
|
||||
```
|
||||
{
|
||||
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]}'
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
```
|
||||
|
||||
|
||||
For solo plans, the tailnet is the email you signed up with.
|
||||
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
|
||||
Her API calls would have the following format:
|
||||
```
|
||||
GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
```
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
### ACL
|
||||
|
||||
<a name=tailnet-acl-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
|
||||
|
||||
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
##### Returns
|
||||
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
|
||||
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
|
||||
|
||||
##### Example
|
||||
|
||||
###### Requesting a HuJSON response:
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/hujson" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
"Tests": [],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": [
|
||||
"*"
|
||||
],
|
||||
"Ports": [
|
||||
"*:*"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/json" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
{
|
||||
"acls" : [
|
||||
{
|
||||
"action" : "accept",
|
||||
"ports" : [
|
||||
"*:*"
|
||||
],
|
||||
"users" : [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups" : {
|
||||
"group:example" : [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
]
|
||||
},
|
||||
"hosts" : {
|
||||
"example-host-1" : "100.100.100.100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
|
||||
|
||||
Sets the ACL for the given domain.
|
||||
HuJSON and JSON are both accepted inputs.
|
||||
An `If-Match` header can be set to avoid missed updates.
|
||||
|
||||
Returns the updated ACL in JSON or HuJSON according to the `Accept` header on success. Otherwise, errors are returned for incorrectly defined ACLs, ACLs with failing tests on attempted updates, and mismatched `If-Match` header and ETag.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
###### POST Body
|
||||
|
||||
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
|
||||
An ACL policy may contain the following top-level properties:
|
||||
|
||||
* `Groups` - Static groups of users which can be used for ACL rules.
|
||||
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
* `ACLs` - Access control lists.
|
||||
* `TagOwners` - Defines who is allowed to use which tags.
|
||||
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on those properties.
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "If-Match: \"e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c\""
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Failed test error response:
|
||||
```
|
||||
{
|
||||
"message": "test(s) failed",
|
||||
"data": [
|
||||
{
|
||||
"user": "user1@example.com",
|
||||
"errors": [
|
||||
"address \"user2@example.com:400\": want: Accept, got: Drop"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-preview-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
|
||||
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
##### 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 paramater 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' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: Returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/devices
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
{
|
||||
"addresses":[
|
||||
"100.68.203.125"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user1-device.example.com",
|
||||
"created":"2020-11-30T22:20:04Z",
|
||||
"lastSeen":"2020-11-30T17:20:04-05:00",
|
||||
"hostname":"User1-Device",
|
||||
"machineKey":"mkey:user1-node-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"expires":"2021-05-29T22:20:04Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
},
|
||||
{
|
||||
"addresses":[
|
||||
"100.111.63.90"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user2-device.example.com",
|
||||
"created":"2020-11-30T22:21:03Z",
|
||||
"lastSeen":"2020-11-30T17:21:03-05:00",
|
||||
"hostname":"User2-Device",
|
||||
"machineKey":"mkey:user2-machine-key",
|
||||
"nodeKey":"nodekey:user2-node-key",
|
||||
"id":"48810",
|
||||
"user":"user2@example.com",
|
||||
"expires":"2021-05-29T22:21:03Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-nameservers-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
|
||||
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
|
||||
Supply the tailnet of interest in the path.
|
||||
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
`dns` - The new list of DNS nameservers in JSON.
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
Returns the new list of nameservers and the status of MagicDNS.
|
||||
|
||||
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
|
||||
|
||||
##### Example
|
||||
###### Adding DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": ["8.8.8.8"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
###### Removing all DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": []}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
"magicDNS": false,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
|
||||
Retrieves the DNS preferences that are currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
|
||||
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
|
||||
If there is at least one DNS server, then MagicDNS can be enabled.
|
||||
Otherwise, it returns an error.
|
||||
Note that removing all nameservers will turn off MagicDNS.
|
||||
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"magicDNS": true}'
|
||||
```
|
||||
|
||||
|
||||
Response:
|
||||
|
||||
If there are no DNS servers, it returns an error message:
|
||||
```
|
||||
{
|
||||
"message":"need at least one nameserver to enable MagicDNS"
|
||||
}
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
|
||||
Retrieves the list of search paths that is currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
|
||||
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`searchPaths` - A list of searchpaths in JSON.
|
||||
```
|
||||
{
|
||||
"searchPaths: ["user1.example.com", "user2.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
}
|
||||
```
|
||||
@@ -11,36 +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"
|
||||
SHORT="$major.$minor.$patch"
|
||||
LONG="${SHORT}$long_suffix"
|
||||
GIT_HASH="$git_hash"
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
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}" "$@"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Runs `go build` with flags configured for docker distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries inside docker, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# 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)
|
||||
|
||||
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 .
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package apitype contains types for the Tailscale local API.
|
||||
package apitype
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
UserProfile *tailcfg.UserProfile
|
||||
}
|
||||
|
||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||
// URL base to do so via.
|
||||
type FileTarget struct {
|
||||
Node *tailcfg.Node
|
||||
|
||||
// PeerAPI is the http://ip:port URL base of the node's peer API,
|
||||
// without any path (not even a single slash).
|
||||
PeerAPIURL string
|
||||
}
|
||||
|
||||
type WaitingFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
}
|
||||
@@ -1,295 +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 tailscale contains Tailscale client code.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// 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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
//
|
||||
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
|
||||
//
|
||||
// The hostname must be "local-tailscaled.sock", even though it
|
||||
// doesn't actually do any DNS lookup. The actual means of connecting to and
|
||||
// authenticating to the local Tailscale daemon vary by platform.
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
return tsClient.Do(req)
|
||||
}
|
||||
|
||||
type errorJSON struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// bestError returns either err, or if body contains a valid JSON
|
||||
// object of type errorJSON, its non-empty error body.
|
||||
func bestError(err error, body []byte) error {
|
||||
var j errorJSON
|
||||
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
|
||||
return errors.New(j.Error)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != wantStatus {
|
||||
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
|
||||
return nil, bestError(err, slurp)
|
||||
}
|
||||
return slurp, nil
|
||||
}
|
||||
|
||||
func get200(ctx context.Context, path string) ([]byte, error) {
|
||||
return send(ctx, "GET", path, 200, nil)
|
||||
}
|
||||
|
||||
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := new(apitype.WhoIsResponse)
|
||||
if err := json.Unmarshal(body, r); err != nil {
|
||||
if max := 200; len(body) > max {
|
||||
body = append(body[:max], "..."...)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
|
||||
func Goroutines(ctx context.Context) ([]byte, error) {
|
||||
return get200(ctx, "/localapi/v0/goroutines")
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
func BugReport(ctx context.Context, note string) (string, error) {
|
||||
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
}
|
||||
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
|
||||
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/status"+queryString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st := new(ipnstate.Status)
|
||||
if err := json.Unmarshal(body, st); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/files/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var wfs []apitype.WaitingFile
|
||||
if err := json.Unmarshal(body, &wfs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wfs, nil
|
||||
}
|
||||
|
||||
func DeleteWaitingFile(ctx context.Context, baseName string) error {
|
||||
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if res.ContentLength == -1 {
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("unexpected chunking")
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
return res.Body, res.ContentLength, nil
|
||||
}
|
||||
|
||||
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/file-targets")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fts []apitype.FileTarget
|
||||
if err := json.Unmarshal(body, &fts); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
return fts, nil
|
||||
}
|
||||
|
||||
func CheckIPForwarding(ctx context.Context) error {
|
||||
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var jres struct {
|
||||
Warning string
|
||||
}
|
||||
if err := json.Unmarshal(body, &jres); err != nil {
|
||||
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
|
||||
}
|
||||
if jres.Warning != "" {
|
||||
return errors.New(jres.Warning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/prefs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p ipn.Prefs
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
mpj, err := json.Marshal(mp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p ipn.Prefs
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func Logout(ctx context.Context) error {
|
||||
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||
// the provided TXT value. The intended use case is answering
|
||||
// LetsEncrypt/ACME dns-01 challenges.
|
||||
//
|
||||
// The control plane will only permit SetDNS requests with very
|
||||
// specific names and values. The name should be
|
||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||
// providing them) and only request new ones as needed; the control plane
|
||||
// rate limits SetDNS requests.
|
||||
//
|
||||
// This is a low-level interface; it's expected that most Tailscale
|
||||
// users use a higher level interface to getting/using TLS
|
||||
// certificates.
|
||||
func SetDNS(ctx context.Context, name, value string) error {
|
||||
v := url.Values{}
|
||||
v.Set("name", name)
|
||||
v.Set("value", value)
|
||||
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
|
||||
// It is intended to be used with netcheck to see availability of DERPs.
|
||||
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
var derpMap tailcfg.DERPMap
|
||||
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = json.Unmarshal(res, &derpMap); err != nil {
|
||||
return nil, fmt.Errorf("invalid derp map json: %w", err)
|
||||
}
|
||||
return &derpMap, nil
|
||||
}
|
||||
@@ -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:]
|
||||
@@ -140,7 +140,7 @@ func main() {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := ioutil.WriteFile(output, out, 0644); err != nil {
|
||||
if err := ioutil.WriteFile(output, out, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -246,9 +246,7 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
|
||||
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
|
||||
writef("\t}")
|
||||
} else if containsPointers(ft.Elem()) {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
writef("\t}")
|
||||
writef("\t\t" + `panic("TODO map value pointers")`)
|
||||
} else {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v", fname)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
func refreshBootstrapDNSLoop() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
for {
|
||||
refreshBootstrapDNS()
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
var r net.Resolver
|
||||
for _, name := range names {
|
||||
addrs, err := r.LookupIP(ctx, "ip", name)
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
@@ -32,7 +35,7 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,12 +48,10 @@ var (
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -58,12 +59,7 @@ func loadConfig() config {
|
||||
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 {
|
||||
@@ -81,8 +77,8 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
func mustNewKey() wgcfg.PrivateKey {
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -101,7 +97,7 @@ func writeNewConfig() config {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0600); err != nil {
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
@@ -128,7 +124,6 @@ func main() {
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
b, err := ioutil.ReadFile(*meshPSKFile)
|
||||
@@ -147,10 +142,9 @@ func main() {
|
||||
}
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// 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) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -159,7 +153,7 @@ func main() {
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://tailscale.com/">Tailscale</a>
|
||||
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
|
||||
<a href="https://godoc.org/tailscale.com/derp">DERP</a>
|
||||
server.
|
||||
</p>
|
||||
`)
|
||||
@@ -167,18 +161,6 @@ 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()
|
||||
@@ -232,6 +214,39 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
pc, err := net.ListenPacket("udp", ":3478")
|
||||
if err != nil {
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -41,36 +38,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return err
|
||||
}
|
||||
c.MeshKey = s.MeshKey()
|
||||
|
||||
// For meshed peers within a region, connect via VPC addresses.
|
||||
c.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
var r net.Resolver
|
||||
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
|
||||
base := strings.TrimSuffix(host, ".tailscale.com")
|
||||
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
vpcHost := base + "-vpc.tailscale.com"
|
||||
ips, _ := r.LookupIP(subCtx, "ip", vpcHost)
|
||||
if len(ips) > 0 {
|
||||
vpcAddr := net.JoinHostPort(ips[0].String(), port)
|
||||
c, err := d.DialContext(subCtx, network, vpcAddr)
|
||||
if err == nil {
|
||||
log.Printf("connected to %v (%v) instead of %v", vpcHost, ips[0], base)
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("failed to connect to %v (%v): %v; trying non-VPC route", vpcHost, ips[0], err)
|
||||
}
|
||||
}
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,354 +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/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"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))
|
||||
}
|
||||
for _, reg := range sortedRegions(lastDERPMap) {
|
||||
for _, from := range reg.Nodes {
|
||||
for _, to := range reg.Nodes {
|
||||
pair := nodePair{from.Name, to.Name}
|
||||
st, ok := state[pair]
|
||||
age := now.Sub(st.at).Round(time.Second)
|
||||
switch {
|
||||
case !ok:
|
||||
o.addBadf("no state for %v", pair)
|
||||
case st.err != nil:
|
||||
o.addBadf("%v: %v", pair, st.err)
|
||||
case age > 90*time.Second:
|
||||
o.addBadf("%v: update is %v old", pair, age)
|
||||
default:
|
||||
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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, 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 {
|
||||
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 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.NewPrivate()
|
||||
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
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
)
|
||||
|
||||
var (
|
||||
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
||||
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
||||
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
||||
)
|
||||
|
||||
//go:embed hello.tmpl.html
|
||||
var embeddedTemplate string
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *testIP != "" {
|
||||
res, err := tailscale.WhoIs(context.Background(), *testIP)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(res)
|
||||
return
|
||||
}
|
||||
if devMode() {
|
||||
// Parse it optimistically
|
||||
var err error
|
||||
tmpl, err = template.New("home").Parse(embeddedTemplate)
|
||||
if err != nil {
|
||||
log.Printf("ignoring template error in dev mode: %v", err)
|
||||
}
|
||||
} else {
|
||||
if embeddedTemplate == "" {
|
||||
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
||||
}
|
||||
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", root)
|
||||
log.Printf("Starting hello server.")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
if *httpAddr != "" {
|
||||
log.Printf("running HTTP server on %s", *httpAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServe(*httpAddr, nil)
|
||||
}()
|
||||
}
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
||||
return tmpl, nil
|
||||
}
|
||||
return template.New("home").Parse(string(tmplData))
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// tmpl is the template used in prod mode.
|
||||
// In dev mode it's only used if the template file doesn't exist on disk.
|
||||
// It's initialized by main after flag parsing.
|
||||
var tmpl *template.Template
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
LoginName string // "foo@bar.com"
|
||||
ProfilePicURL string // "https://..."
|
||||
MachineName string // "imac5k"
|
||||
MachineOS string // "Linux"
|
||||
IP string // "100.2.3.4"
|
||||
}
|
||||
|
||||
func tailscaleIP(who *apitype.WhoIsResponse) string {
|
||||
if who == nil {
|
||||
return ""
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP().String()
|
||||
}
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP().String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.RequestURI != "/" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.Error(w, "template error: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||
var data tmplData
|
||||
if err != nil {
|
||||
if devMode() {
|
||||
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
|
||||
data = tmplData{
|
||||
DisplayName: "Taily Scalerson",
|
||||
LoginName: "taily@scaler.son",
|
||||
ProfilePicURL: "https://placekitten.com/200/200",
|
||||
MachineName: "scaled",
|
||||
MachineOS: "Linux",
|
||||
IP: "100.1.2.3",
|
||||
}
|
||||
} else {
|
||||
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
data = tmplData{
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
||||
MachineName: firstLabel(who.Node.ComputedName),
|
||||
MachineOS: who.Node.Hostinfo.OS,
|
||||
IP: tailscaleIP(who),
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// firstLabel s up until the first period, if any.
|
||||
func firstLabel(s string) string {
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<title>Hello from Tailscale</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
width: 95%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pr-3 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mb-12 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-width-0 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-t-1 {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
border-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: #eeebea;
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.bg-gray-0 {
|
||||
background-color: #faf9f8;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #0d4b3b;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #3f5db3;
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
color: #253570;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #444342;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #2e2d2d;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #232222;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.font-title {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 9999px;
|
||||
background-size: cover;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animate .panel {
|
||||
transform: translateY(10%);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
|
||||
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .panel-interior {
|
||||
opacity: 0.0;
|
||||
transition: opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .logo {
|
||||
transform: translateY(2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-title {
|
||||
transform: translateY(1.6rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-text {
|
||||
transform: translateY(1.2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .footer {
|
||||
transform: translateY(-0.5rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animating .panel {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animating .panel-interior {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.animating .spinner {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.animating .logo,
|
||||
.animating .header-title,
|
||||
.animating .header-text,
|
||||
.animating .footer {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
align-items: center;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.spinner span {
|
||||
display: inline-block;
|
||||
background-color: currentColor;
|
||||
border-radius: 9999px;
|
||||
animation-name: loading-dots-blink;
|
||||
animation-duration: 1.4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode: both;
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(2) {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(3) {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animate .spinner {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@keyframes loading-dots-blink {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
* {
|
||||
animation-duration: 0ms !important;
|
||||
transition-duration: 0ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<script>
|
||||
(function() {
|
||||
var lastSeen = localStorage.getItem("lastSeen");
|
||||
if (!lastSeen) {
|
||||
document.body.classList.add("animate");
|
||||
window.addEventListener("load", function () {
|
||||
setTimeout(function () {
|
||||
document.body.classList.add("animating");
|
||||
localStorage.setItem("lastSeen", Date.now());
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<main class="text-gray-800">
|
||||
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
</svg>
|
||||
<header class="mb-8 text-center">
|
||||
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
|
||||
<p class="header-text">This device is signed in as…</p>
|
||||
</header>
|
||||
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
|
||||
<div class="spinner text-gray-600">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
|
||||
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
|
||||
<div class="overflow-hidden">
|
||||
{{ with .DisplayName }}
|
||||
<h4 class="font-semibold truncate">{{.}}</h4>
|
||||
{{ end }}
|
||||
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer text-gray-600 text-center mb-12">
|
||||
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
|
||||
target="_blank">what you can do next →</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -55,13 +55,9 @@ func main() {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux) // registers /debug/*
|
||||
mux := tsweb.NewMux(http.HandlerFunc(debugHandler))
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
|
||||
Quiet200s: true,
|
||||
Logf: log.Printf,
|
||||
})))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, log.Printf)))
|
||||
|
||||
ch := &certHolder{
|
||||
hostname: *hostname,
|
||||
@@ -171,3 +167,23 @@ func (c *certHolder) loadLocked() error {
|
||||
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>
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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/v2/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
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dsnet/golib/jsonfmt"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
)
|
||||
|
||||
const tailscaleAPIURL = "https://api.tailscale.com/api"
|
||||
|
||||
var adminCmd = &ffcli.Command{
|
||||
Name: "admin",
|
||||
ShortUsage: "admin <subcommand> [command flags]",
|
||||
ShortHelp: "Administrate a tailnet",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The "tailscale admin" command administrates a tailnet through the CLI.
|
||||
It is a wrapper over the RESTful API served at ` + tailscaleAPIURL + `.
|
||||
See https://github.com/tailscale/tailscale/blob/main/api.md for more information
|
||||
about the API itself.
|
||||
|
||||
In order for the "admin" command to call the API, it needs an API key,
|
||||
which is specified by setting the TAILSCALE_API_KEY environment variable.
|
||||
Also, to easy usage, the tailnet to administrate can be specified through the
|
||||
TAILSCALE_NET_NAME environment variable, or specified with the -tailnet flag.
|
||||
|
||||
Visit https://login.tailscale.com/admin/settings/authkeys in order to obtain
|
||||
an API key.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||
// TODO(dsnet): Can we determine the default tailnet from what this
|
||||
// device is currently part of? Alternatively, when add specific logic
|
||||
// to handle auth keys, we can always associate a given key with a
|
||||
// specific tailnet.
|
||||
fs.StringVar(&adminArgs.tailnet, "tailnet", os.Getenv("TAILSCALE_NET_NAME"), "which tailnet to administrate")
|
||||
return fs
|
||||
})(),
|
||||
// TODO(dsnet): Handle users, groups, dns.
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "acl",
|
||||
ShortUsage: "acl <subcommand> [command flags]",
|
||||
ShortHelp: "Manage the ACL for a tailnet",
|
||||
// TODO(dsnet): Handle preview.
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "get",
|
||||
ShortUsage: "get",
|
||||
ShortHelp: "Downloads the HuJSON ACL file to stdout",
|
||||
Exec: checkAdminKey(runAdminACLGet),
|
||||
}, {
|
||||
Name: "set",
|
||||
ShortUsage: "set",
|
||||
ShortHelp: "Uploads the HuJSON ACL file from stdin",
|
||||
Exec: checkAdminKey(runAdminACLSet),
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}, {
|
||||
Name: "devices",
|
||||
ShortUsage: "devices <subcommand> [command flags]",
|
||||
ShortHelp: "Manage devices in a tailnet",
|
||||
Subcommands: []*ffcli.Command{{
|
||||
Name: "list",
|
||||
ShortUsage: "list",
|
||||
ShortHelp: "List all devices in a tailnet",
|
||||
Exec: checkAdminKey(runAdminDevicesList),
|
||||
}, {
|
||||
Name: "get",
|
||||
ShortUsage: "get <id>",
|
||||
ShortHelp: "Get information about a specific device",
|
||||
Exec: checkAdminKey(runAdminDevicesGet),
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}},
|
||||
Exec: runHelp,
|
||||
}
|
||||
|
||||
var adminArgs struct {
|
||||
tailnet string // which tailnet to operate upon
|
||||
}
|
||||
|
||||
func checkAdminKey(f func(context.Context, string, []string) error) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
// TODO(dsnet): We should have a subcommand or flag to manage keys.
|
||||
// Use of an environment variable is a temporary hack.
|
||||
key := os.Getenv("TAILSCALE_API_KEY")
|
||||
if !strings.HasPrefix(key, "tskey-") {
|
||||
return errors.New("no API key specified")
|
||||
}
|
||||
return f(ctx, key, args)
|
||||
}
|
||||
}
|
||||
|
||||
func runAdminACLGet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/acl", nil, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminACLSet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodPost, "/v2/tailnet/"+adminArgs.tailnet+"/acl", os.Stdin, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminDevicesList(ctx context.Context, key string, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/devices", nil, os.Stdout)
|
||||
}
|
||||
|
||||
func runAdminDevicesGet(ctx context.Context, key string, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
return adminCallAPI(ctx, key, http.MethodGet, "/v2/device/"+args[0], nil, os.Stdout)
|
||||
}
|
||||
|
||||
func adminCallAPI(ctx context.Context, key, method, path string, in io.Reader, out io.Writer) error {
|
||||
req, err := http.NewRequestWithContext(ctx, method, tailscaleAPIURL+path, in)
|
||||
req.SetBasicAuth(key, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send HTTP request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to receive HTTP response: %w", err)
|
||||
}
|
||||
b, err = jsonfmt.Format(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to format JSON response: %w", err)
|
||||
}
|
||||
_, err = out.Write(b)
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var bugReportCmd = &ffcli.Command{
|
||||
Name: "bugreport",
|
||||
Exec: runBugReport,
|
||||
ShortHelp: "Print a shareable identifier to help diagnose issues",
|
||||
ShortUsage: "bugreport [note]",
|
||||
}
|
||||
|
||||
func runBugReport(ctx context.Context, args []string) error {
|
||||
var note string
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
note = args[0]
|
||||
default:
|
||||
return errors.New("unknown argumets")
|
||||
}
|
||||
logMarker, err := tailscale.BugReport(ctx, note)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(logMarker)
|
||||
return nil
|
||||
}
|
||||
@@ -8,78 +8,37 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
// ActLikeCLI reports whether a GUI application should act like the
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
func ActLikeCLI() bool {
|
||||
// This function is only used on macOS.
|
||||
if runtime.GOOS != "darwin" {
|
||||
if len(os.Args) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Escape hatch to let people force running the macOS
|
||||
// GUI Tailscale binary as the CLI.
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v {
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
|
||||
// If our parent is launchd, we're definitely not
|
||||
// being run as a CLI.
|
||||
if os.Getppid() == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Xcode adds the -NSDocumentRevisionsDebugMode flag on execution.
|
||||
// If present, we are almost certainly being run as a GUI.
|
||||
for _, arg := range os.Args {
|
||||
if arg == "-NSDocumentRevisionsDebugMode" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Looking at the environment of the GUI Tailscale app (ps eww
|
||||
// $PID), empirically none of these environment variables are
|
||||
// present. But all or some of these should be present with
|
||||
// Terminal.all and bash or zsh.
|
||||
for _, e := range []string{
|
||||
"SHLVL",
|
||||
"TERM",
|
||||
"TERM_PROGRAM",
|
||||
"PS1",
|
||||
} {
|
||||
if os.Getenv(e) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func runHelp(context.Context, []string) error {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@@ -91,34 +50,22 @@ func Run(args []string) error {
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
For help on subcommands, add --help after: "tailscale status --help".
|
||||
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
logoutCmd,
|
||||
adminCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: runHelp,
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
c.UsageFunc = usageFunc
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
@@ -130,8 +77,6 @@ change in the future.
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
@@ -148,15 +93,13 @@ var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
}
|
||||
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
@@ -167,14 +110,7 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-interrupt:
|
||||
case <-ctx.Done():
|
||||
// Context canceled elsewhere.
|
||||
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
||||
return
|
||||
}
|
||||
gotSignal.Set(true)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
@@ -184,22 +120,19 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return fmt.Errorf("%w (tailscaled stopped running?)", err)
|
||||
}
|
||||
return err
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
@@ -210,72 +143,3 @@ func strSliceContains(ss []string, s string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func usageFunc(c *ffcli.Command) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "USAGE\n")
|
||||
if c.ShortUsage != "" {
|
||||
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s\n", c.Name)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
if c.LongHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
|
||||
}
|
||||
|
||||
if len(c.Subcommands) > 0 {
|
||||
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
for _, subcommand := range c.Subcommands {
|
||||
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
if countFlags(c.FlagSet) > 0 {
|
||||
fmt.Fprintf(&b, "FLAGS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
var s string
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
if isBoolFlag(f) {
|
||||
s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
|
||||
} else {
|
||||
s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
|
||||
if len(name) > 0 {
|
||||
s += " " + name
|
||||
}
|
||||
}
|
||||
// Four spaces before the tab triggers good alignment
|
||||
// for both 4- and 8-space tab stops.
|
||||
s += "\n \t"
|
||||
s += strings.ReplaceAll(usage, "\n", "\n \t")
|
||||
|
||||
if f.DefValue != "" {
|
||||
s += fmt.Sprintf(" (default %s)", f.DefValue)
|
||||
}
|
||||
|
||||
fmt.Fprintln(&b, s)
|
||||
})
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func isBoolFlag(f *flag.Flag) bool {
|
||||
bf, ok := f.Value.(interface {
|
||||
IsBoolFlag() bool
|
||||
})
|
||||
return ok && bf.IsBoolFlag()
|
||||
}
|
||||
|
||||
func countFlags(fs *flag.FlagSet) (n int) {
|
||||
fs.VisitAll(func(*flag.Flag) { n++ })
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
// But it should include anything handled specially (e.g. linux, windows)
|
||||
// and at least one thing that's not (darwin, freebsd).
|
||||
var geese = []string{"linux", "darwin", "windows", "freebsd"}
|
||||
|
||||
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
|
||||
// all flags. This will panic if a new flag creeps in that's unhandled.
|
||||
//
|
||||
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
|
||||
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
got := mp.Pretty()
|
||||
wantEmpty := preflessFlag(f.Name)
|
||||
isEmpty := got == "MaskedPrefs{}"
|
||||
if isEmpty != wantEmpty {
|
||||
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string // argv to be parsed by FlagSet
|
||||
curPrefs *ipn.Prefs
|
||||
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "losing_hostname",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_explicitly",
|
||||
flags: []string{"--hostname=bar"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flags: []string{"--hostname="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
|
||||
// a fresh server's initial prefs.
|
||||
name: "up_with_default_prefs",
|
||||
flags: []string{"--authkey=foosdlkfjskdljf"},
|
||||
curPrefs: ipn.NewPrefs(),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_change",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_matches_shell_user",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "alice",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "error_advertised_routes_exit_node_removed",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_exit_node_removed_explicit",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
||||
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node", // Issue 1859
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes_and_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "exit_node_clearing", // Issue 1777
|
||||
flags: []string{"--exit-node="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "fooID",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit",
|
||||
flags: []string{"--force-reauth"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit_except_hostname",
|
||||
flags: []string{"--hostname=newhostname"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "loggedout_is_implicit",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "", // not an error. LoggedOut is implicit.
|
||||
},
|
||||
{
|
||||
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
|
||||
// values is able to enable exit nodes without warnings.
|
||||
name: "make_windows_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "ignore_netfilter_change_non_linux",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "errors_preserve_explicit_flags",
|
||||
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: accidentalUpPrefix + " --authkey=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_ip_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_id_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
{
|
||||
name: "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",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
goos := "linux"
|
||||
if tt.goos != "" {
|
||||
goos = tt.goos
|
||||
}
|
||||
var upArgs upArgsT
|
||||
flagSet := newUpFlagSet(goos, &upArgs)
|
||||
flagSet.Parse(tt.flags)
|
||||
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
|
||||
fs := newUpFlagSet(goos, &args)
|
||||
fs.Parse(flagArgs) // populates args
|
||||
return
|
||||
}
|
||||
|
||||
func TestPrefsFromUpArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upArgsT
|
||||
goos string // runtime.GOOS; empty means linux
|
||||
st *ipnstate.Status // or nil
|
||||
want *ipn.Prefs
|
||||
wantErr string
|
||||
wantWarn string
|
||||
}{
|
||||
{
|
||||
name: "default_linux",
|
||||
goos: "linux",
|
||||
args: upArgsFromOSArgs("linux"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default_windows",
|
||||
goos: "windows",
|
||||
args: upArgsFromOSArgs("windows"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "advertise_default_route",
|
||||
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_invalid_ip",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "foo",
|
||||
},
|
||||
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_unmasked_bits",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "1.2.3.4/16",
|
||||
},
|
||||
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_bad_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
args: upArgsT{
|
||||
exitNodeAllowLANAccess: true,
|
||||
},
|
||||
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
|
||||
},
|
||||
{
|
||||
name: "error_tag_prefix",
|
||||
args: upArgsT{
|
||||
advertiseTags: "foo",
|
||||
},
|
||||
wantErr: `tag: "foo": tags must start with 'tag:'`,
|
||||
},
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
args: upArgsT{
|
||||
netfilterMode: "",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode=""`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_bogus",
|
||||
args: upArgsT{
|
||||
netfilterMode: "bogus",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode="bogus"`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_ip_is_self_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "100.105.106.107",
|
||||
},
|
||||
st: &ipnstate.Status{
|
||||
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
|
||||
},
|
||||
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_nodivert",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "nodivert",
|
||||
},
|
||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_off",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
}
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.want == nil {
|
||||
t.Fatal("tt.want is nil")
|
||||
}
|
||||
if !got.Equals(tt.want) {
|
||||
jgot, _ := json.MarshalIndent(got, "", "\t")
|
||||
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
if bytes.Equal(jgot, jwant) {
|
||||
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
|
||||
}
|
||||
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
|
||||
got.Pretty(), tt.want.Pretty(),
|
||||
jgot, jwant,
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPrefFlagMapping(t *testing.T) {
|
||||
prefHasFlag := map[string]bool{}
|
||||
for _, pv := range prefsOfFlag {
|
||||
for _, pref := range pv {
|
||||
prefHasFlag[pref] = true
|
||||
}
|
||||
}
|
||||
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if prefHasFlag[prefName] {
|
||||
continue
|
||||
}
|
||||
switch prefName {
|
||||
case "WantRunning", "Persist", "LoggedOut":
|
||||
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
||||
continue
|
||||
case "OSVersion", "DeviceModel":
|
||||
// Only used by Android, which doesn't have a CLI mode anyway, so
|
||||
// fine to not map.
|
||||
continue
|
||||
case "NotepadURLs":
|
||||
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagAppliesToOS(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if !flagAppliesToOS(f.Name, goos) {
|
||||
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePrefs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string // argv to be parsed into env.flagSet and env.upArgs
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
wantSimpleUp bool
|
||||
wantJustEditMP *ipn.MaskedPrefs
|
||||
wantErrSubtr string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{
|
||||
backendState: "Stopped",
|
||||
},
|
||||
wantSimpleUp: true,
|
||||
},
|
||||
{
|
||||
name: "just_edit",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "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",
|
||||
},
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -6,21 +6,26 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -28,116 +33,143 @@ var debugCmd = &ffcli.Command{
|
||||
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.derpMap, "derp", false, "If true, dump DERP map")
|
||||
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
localCreds bool
|
||||
goroutines bool
|
||||
ipn bool
|
||||
netMap bool
|
||||
derpMap bool
|
||||
file string
|
||||
prefs bool
|
||||
pretty bool
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
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.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
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.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
return nil
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
if debugArgs.derpMap {
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.ipn {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !debugArgs.netMap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
if debugArgs.file != "" {
|
||||
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
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -6,12 +6,10 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
@@ -28,19 +26,41 @@ func runDown(ctx context.Context, args []string) error {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
}
|
||||
if st.BackendState == "Stopped" {
|
||||
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
|
||||
return nil
|
||||
}
|
||||
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: false,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, func() {
|
||||
log.Fatalf("timeout running stop")
|
||||
})
|
||||
return err
|
||||
defer timer.Stop()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
cur := n.Status.BackendState
|
||||
switch cur {
|
||||
case "Stopped":
|
||||
log.Printf("already stopped")
|
||||
cancel()
|
||||
default:
|
||||
log.Printf("was in state %q", cur)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.State != nil {
|
||||
log.Printf("now in state %q", *n.State)
|
||||
if *n.State == ipn.Stopped {
|
||||
cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
bc.SetWantRunning(false)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var fileCmd = &ffcli.Command{
|
||||
Name: "file",
|
||||
ShortUsage: "file <cp|get> ...",
|
||||
ShortHelp: "Send or receive files",
|
||||
Subcommands: []*ffcli.Command{
|
||||
fileCpCmd,
|
||||
fileGetCmd,
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
// TODO(bradfitz): is there a better ffcli way to
|
||||
// annotate subcommand-required commands that don't
|
||||
// have an exec body of their own?
|
||||
return errors.New("file subcommand required; run 'tailscale file -h' for details")
|
||||
},
|
||||
}
|
||||
|
||||
var fileCpCmd = &ffcli.Command{
|
||||
Name: "cp",
|
||||
ShortUsage: "file cp <files...> <target>:",
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := 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")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var cpArgs struct {
|
||||
name string
|
||||
verbose bool
|
||||
targets bool
|
||||
}
|
||||
|
||||
func runCp(ctx context.Context, args []string) error {
|
||||
if cpArgs.targets {
|
||||
return runCpTargets(ctx, args)
|
||||
}
|
||||
if len(args) < 2 {
|
||||
return errors.New("usage: tailscale file cp <files...> <target>:")
|
||||
}
|
||||
files, target := args[:len(args)-1], args[len(args)-1]
|
||||
if !strings.HasSuffix(target, ":") {
|
||||
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
|
||||
}
|
||||
target = strings.TrimSuffix(target, ":")
|
||||
hadBrackets := false
|
||||
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
|
||||
hadBrackets = true
|
||||
target = strings.TrimSuffix(strings.TrimPrefix(target, "["), "]")
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(target); err == nil && ip.Is6() && !hadBrackets {
|
||||
return fmt.Errorf("an IPv6 literal must be written as [%s]", ip)
|
||||
} else if hadBrackets && (err != nil || !ip.Is6()) {
|
||||
return errors.New("unexpected brackets around target")
|
||||
}
|
||||
ip, err := tailscaleIPFromArg(ctx, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
if isOffline {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
if cpArgs.name != "" {
|
||||
return errors.New("can't use --name= with multiple files")
|
||||
}
|
||||
for _, fileArg := range files {
|
||||
if fileArg == "-" {
|
||||
return errors.New("can't use '-' as STDIN file when providing filename arguments")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileArg := range files {
|
||||
var fileContents io.Reader
|
||||
var name = cpArgs.name
|
||||
var contentLength int64 = -1
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(fileArg)
|
||||
if err != nil {
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("the GUI version of Tailscale on macOS runs in a macOS sandbox that can't read files")
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return errors.New("directories not supported")
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
fileContents = io.LimitReader(f, contentLength)
|
||||
if name == "" {
|
||||
name = filepath.Base(fileArg)
|
||||
}
|
||||
|
||||
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
|
||||
fileContents = &slowReader{r: fileContents}
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.IP() != ip {
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
}
|
||||
|
||||
// fileTargetErrorDetail returns a non-nil error saying why ip is an
|
||||
// invalid file sharing target.
|
||||
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
|
||||
found := false
|
||||
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
|
||||
for _, peer := range st.Peer {
|
||||
for _, pip := range peer.TailscaleIPs {
|
||||
if pip == ip {
|
||||
found = true
|
||||
if peer.UserID != st.Self.UserID {
|
||||
return errors.New("owned by different user; can only send files to your own devices")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return errors.New("target seems to be running an old Tailscale version")
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
|
||||
}
|
||||
return errors.New("unknown target; not in your Tailnet")
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
func ext(b []byte) string {
|
||||
if len(b) < maxSniff && utf8.Valid(b) {
|
||||
return ".txt"
|
||||
}
|
||||
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
}
|
||||
|
||||
type slowReader struct {
|
||||
r io.Reader
|
||||
rl *rate.Limiter
|
||||
}
|
||||
|
||||
func (r *slowReader) Read(p []byte) (n int, err error) {
|
||||
const burst = 4 << 10
|
||||
plen := len(p)
|
||||
if plen > burst {
|
||||
plen = burst
|
||||
}
|
||||
if r.rl == nil {
|
||||
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
|
||||
}
|
||||
n, err = r.r.Read(p[:plen])
|
||||
r.rl.WaitN(context.Background(), n)
|
||||
return
|
||||
}
|
||||
|
||||
func runCpTargets(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("invalid arguments with --targets")
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
var detail string
|
||||
if n.Online != nil {
|
||||
if !*n.Online {
|
||||
detail = "offline"
|
||||
}
|
||||
} else {
|
||||
detail = "unknown-status"
|
||||
}
|
||||
if detail != "" && n.LastSeen != nil {
|
||||
d := time.Since(*n.LastSeen)
|
||||
detail += fmt.Sprintf("; last seen %v ago", d.Round(time.Minute))
|
||||
}
|
||||
if detail != "" {
|
||||
detail = "\t" + detail
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var fileGetCmd = &ffcli.Command{
|
||||
Name: "get",
|
||||
ShortUsage: "file get [--wait] [--verbose] <target-directory>",
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
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
|
||||
})(),
|
||||
}
|
||||
|
||||
var getArgs struct {
|
||||
wait bool
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runFileGet(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: file get <target-directory>")
|
||||
}
|
||||
log.SetFlags(0)
|
||||
|
||||
dir := args[0]
|
||||
if dir == "/dev/null" {
|
||||
return wipeInbox(ctx)
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", dir)
|
||||
}
|
||||
|
||||
var wfs []apitype.WaitingFile
|
||||
var err error
|
||||
for {
|
||||
wfs, err = tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
if len(wfs) != 0 || !getArgs.wait {
|
||||
break
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("waiting for file...")
|
||||
}
|
||||
if err := waitForFile(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening inbox file %q: %v", wf.Name, err)
|
||||
}
|
||||
targetFile := filepath.Join(dir, wf.Name)
|
||||
of, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(targetFile); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite %v", targetFile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(of, rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write %v: %v", targetFile, err)
|
||||
}
|
||||
if err := of.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("wrote %v (%d bytes)", wf.Name, size)
|
||||
}
|
||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||
return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("moved %d files", deleted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wipeInbox(ctx context.Context) error {
|
||||
if getArgs.wait {
|
||||
return errors.New("can't use --wait with /dev/null target")
|
||||
}
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
if getArgs.verbose {
|
||||
log.Printf("deleting %v ...", wf.Name)
|
||||
}
|
||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||
return fmt.Errorf("deleting %q: %v", wf.Name, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("deleted %d files", deleted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForFile(ctx context.Context) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
fileWaiting := make(chan bool, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
case fileWaiting <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
go pump(pumpCtx, bc, c)
|
||||
select {
|
||||
case <-fileWaiting:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
return pumpCtx.Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var ipCmd = &ffcli.Command{
|
||||
Name: "ip",
|
||||
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 := 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
|
||||
})(),
|
||||
}
|
||||
|
||||
var ipArgs struct {
|
||||
want4 bool
|
||||
want6 bool
|
||||
}
|
||||
|
||||
func runIP(ctx context.Context, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
var of string
|
||||
if len(args) == 1 {
|
||||
of = args[0]
|
||||
}
|
||||
|
||||
v4, v6 := ipArgs.want4, ipArgs.want6
|
||||
if v4 && v6 {
|
||||
return errors.New("tailscale up -4 and -6 are mutually exclusive")
|
||||
}
|
||||
if !v4 && !v6 {
|
||||
v4, v6 = true, true
|
||||
}
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ips := st.TailscaleIPs
|
||||
if of != "" {
|
||||
ip, err := tailscaleIPFromArg(ctx, of)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer, ok := peerMatchingIP(st, ip)
|
||||
if !ok {
|
||||
return fmt.Errorf("no peer found with IP %v", ip)
|
||||
}
|
||||
ips = peer.TailscaleIPs
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
match = true
|
||||
fmt.Println(ip)
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
if ipArgs.want4 {
|
||||
return errors.New("no Tailscale IPv4 address")
|
||||
}
|
||||
if ipArgs.want6 {
|
||||
return errors.New("no Tailscale IPv6 address")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, ps = range st.Peer {
|
||||
for _, pip := range ps.TailscaleIPs {
|
||||
if ip == pip {
|
||||
return ps, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var logoutCmd = &ffcli.Command{
|
||||
Name: "logout",
|
||||
ShortUsage: "logout [flags]",
|
||||
ShortHelp: "Disconnect from Tailscale and expire current node key",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
"tailscale logout" brings the network down and invalidates
|
||||
the current node key, forcing a future use of it to cause
|
||||
a reauthentication.
|
||||
`),
|
||||
Exec: runLogout,
|
||||
}
|
||||
|
||||
func runLogout(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
return tailscale.Logout(ctx)
|
||||
}
|
||||
@@ -9,20 +9,16 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -49,8 +45,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),
|
||||
DNSCache: dnscache.Get(),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
@@ -63,13 +58,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
}
|
||||
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
dm, err = prodDERPMap(ctx, http.DefaultClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dm := derpmap.Prod()
|
||||
for {
|
||||
t0 := time.Now()
|
||||
report, err := c.GetReport(ctx, dm)
|
||||
@@ -186,27 +175,3 @@ func portMapping(r *netcheck.Report) string {
|
||||
}
|
||||
return strings.Join(got, ", ")
|
||||
}
|
||||
|
||||
func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create prodDERPMap request: %w", err)
|
||||
}
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b)
|
||||
}
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err = json.Unmarshal(b, &derpMap); err != nil {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap: %w", err)
|
||||
}
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
@@ -48,7 +47,6 @@ relay node.
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
@@ -59,7 +57,6 @@ var pingArgs struct {
|
||||
num int
|
||||
untilDirect bool
|
||||
verbose bool
|
||||
tsmp bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
@@ -67,44 +64,44 @@ func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
ch <- pr
|
||||
}
|
||||
})
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(ctx, bc, c) }()
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
n++
|
||||
bc.Ping(ip, pingArgs.tsmp)
|
||||
bc.Ping(ip)
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
case pr := <-prc:
|
||||
case pr := <-ch:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
@@ -114,20 +111,8 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr.DERPRegionID != 0 {
|
||||
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
|
||||
}
|
||||
if pingArgs.tsmp {
|
||||
// TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
|
||||
// For now just say it came via TSMP.
|
||||
via = "TSMP"
|
||||
}
|
||||
anyPong = true
|
||||
extra := ""
|
||||
if pr.PeerAPIPort != 0 {
|
||||
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
|
||||
}
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
if pr.Endpoint != "" && pingArgs.untilDirect {
|
||||
return nil
|
||||
}
|
||||
@@ -139,41 +124,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
if pingArgs.untilDirect {
|
||||
return errors.New("direct connection not established")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
return hostOrIP, nil
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
if len(ps.TailscaleIPs) == 0 {
|
||||
return "", errors.New("node found but lacks an IP")
|
||||
}
|
||||
return ps.TailscaleIPs[0].String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return "", fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return "", fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
return addrs[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,22 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortUsage: "status [-active] [-web] [-json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
@@ -36,8 +34,7 @@ var statusCmd = &ffcli.Command{
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
})(),
|
||||
@@ -50,18 +47,42 @@ var statusArgs struct {
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
peers bool // in CLI mode, show status of peer machines
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
st, err := tailscale.Status(ctx)
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
ch := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
ch <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
getStatus := func() (*ipnstate.Status, error) {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-ch:
|
||||
return st, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
st, err := getStatus()
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
return err
|
||||
}
|
||||
if statusArgs.json {
|
||||
if statusArgs.active {
|
||||
for peer, ps := range st.Peer {
|
||||
if !ps.Active {
|
||||
if !peerActive(ps) {
|
||||
delete(st.Peer, peer)
|
||||
}
|
||||
}
|
||||
@@ -92,7 +113,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
st, err := tailscale.Status(ctx)
|
||||
st, err := getStatus()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -106,58 +127,39 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
os.Exit(1)
|
||||
case ipn.Stopped.String():
|
||||
if st.BackendState == ipn.Stopped.String() {
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
fmt.Println("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
fmt.Println("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
firstIPString(ps.TailscaleIPs),
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
ownerLogin(st, ps),
|
||||
active := peerActive(ps)
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
ps.PublicKey.ShortString(),
|
||||
ps.OS,
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
ps.TxBytes,
|
||||
ps.RxBytes,
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
if active && relay != "" && ps.CurAddr == "" {
|
||||
relay = "*" + relay + "*"
|
||||
} else {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
relay = " " + relay
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
f("%-6s", relay)
|
||||
for i, addr := range ps.Addrs {
|
||||
if i != 0 {
|
||||
f(", ")
|
||||
}
|
||||
if addr == ps.CurAddr {
|
||||
f("*%s*", addr)
|
||||
} else {
|
||||
f("%s", addr)
|
||||
}
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
@@ -165,52 +167,21 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
if statusArgs.peers {
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
if statusArgs.active && !ps.Active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
return baseName
|
||||
}
|
||||
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if ps.UserID.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
u, ok := st.User[ps.UserID]
|
||||
if !ok {
|
||||
return fmt.Sprint(ps.UserID)
|
||||
}
|
||||
if i := strings.Index(u.LoginName, "@"); i != -1 {
|
||||
return u.LoginName[:i+1]
|
||||
}
|
||||
return u.LoginName
|
||||
}
|
||||
|
||||
func firstIPString(v []netaddr.IP) string {
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
return v[0].String()
|
||||
// peerActive reports whether ps has recent activity.
|
||||
//
|
||||
// TODO: have the server report this bool instead.
|
||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -42,10 +42,29 @@ func runVersion(ctx context.Context, args []string) error {
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
|
||||
st, err := tailscale.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
fmt.Printf("Daemon: %s\n", n.Version)
|
||||
close(done)
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
fmt.Printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,388 +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"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"net/url"
|
||||
"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/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
|
||||
}
|
||||
|
||||
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 := 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
|
||||
}
|
||||
|
||||
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" {
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
return
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(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,
|
||||
}
|
||||
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 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()
|
||||
|
||||
<-pumpCtx.Done() // wait for authURL or complete failure
|
||||
if authURL == "" && retErr == nil {
|
||||
retErr = pumpCtx.Err()
|
||||
}
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
return authURL, retErr
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
<!doctype html>
|
||||
<html class="bg-gray-50">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
|
||||
<title>Tailscale</title>
|
||||
<style>{{template "web.css"}}</style>
|
||||
</head>
|
||||
|
||||
<body class="py-14">
|
||||
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
|
||||
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0 mr-4">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
</svg>
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
|
||||
{{ with .Profile.ProfilePicURL }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
|
||||
style="background-image: url('{{.}}'); background-size: cover;"></div>
|
||||
{{ else }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{ if .IP }}
|
||||
<div
|
||||
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
|
||||
{{ if .IP }}
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
|
||||
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Reauthenticate</button>
|
||||
</a>
|
||||
{{ else }}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
|
||||
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or, learn more at <a
|
||||
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Log In</button>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ else if eq .Status "NeedsMachineAuth" }}
|
||||
<div class="mb-4">
|
||||
This device is authorized, but needs approval from a network admin before it can connect to the network.
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="mb-4">
|
||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
{{ end }}
|
||||
</main>
|
||||
<script>(function () {
|
||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
||||
let fetchingUrl = false;
|
||||
|
||||
function handleClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (fetchingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchingUrl = true;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("SynoToken");
|
||||
const nextParams = new URLSearchParams({ up: true });
|
||||
if (token) {
|
||||
nextParams.set("SynoToken", token)
|
||||
}
|
||||
const nextUrl = new URL(window.location);
|
||||
nextUrl.search = nextParams.toString()
|
||||
const url = nextUrl.toString();
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
const url = res["url"];
|
||||
if (url) {
|
||||
document.location.href = url;
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}).catch(err => {
|
||||
alert("Failed to log in: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
Array.from(loginButtons).forEach(el => {
|
||||
el.addEventListener("click", handleClick);
|
||||
})
|
||||
})();</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,133 @@
|
||||
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/dsnet/golib/jsonfmt from tailscale.com/cmd/tailscale/cli
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW 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/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
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/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr 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/atomicfile from tailscale.com/ipn+
|
||||
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
|
||||
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/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/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+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp
|
||||
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/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/gzip from net/http+
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -131,7 +155,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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+
|
||||
@@ -139,7 +162,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
@@ -147,37 +169,34 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
io/ioutil from crypto/tls+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from math/big+
|
||||
mime from mime/multipart+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember
|
||||
L os/user from github.com/godbus/dbus/v5
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
@@ -185,8 +204,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -8,19 +8,20 @@ package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
if err := cli.Run(args); err != nil {
|
||||
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
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.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
return errors.New("unknown non-flag debug subcommand arguments")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
return
|
||||
}
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump(mon.InterfaceState())
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create derp map request: %w", err)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
|
||||
}
|
||||
var dmap tailcfg.DERPMap
|
||||
if err = json.Unmarshal(b, &dmap); err != nil {
|
||||
return fmt.Errorf("fetch DERP map: %w", err)
|
||||
}
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,56 @@
|
||||
tailscale.com/cmd/tailscaled 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/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 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 tailscale.com/wgengine/monitor+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
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/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
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+
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
💣 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/winpipe from golang.zx2c4.com/wireguard/ipc
|
||||
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device+
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun+
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
|
||||
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/tcpip+
|
||||
inet.af/netstack/state/wire from inet.af/netstack/state
|
||||
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
|
||||
inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
|
||||
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack+
|
||||
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
|
||||
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
|
||||
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/tcpconntrack from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
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/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
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/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/policy 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
|
||||
@@ -111,75 +58,47 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/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/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 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/wgengine+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
|
||||
tailscale.com/net/socks5/tssocks from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/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/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/net/tstun+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/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/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/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/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/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+
|
||||
@@ -188,38 +107,38 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.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/crypto/ssh/terminal from tailscale.com/logpolicy
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/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 tailscale.com/derp+
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from 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
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
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+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -246,7 +165,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -254,7 +172,6 @@ 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/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
@@ -262,25 +179,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
html from html/template+
|
||||
html/template from net/http/pprof
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
io/ioutil from crypto/tls+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from mime/multipart+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/httputil from tailscale.com/ipn/localapi
|
||||
net/http/internal from net/http+
|
||||
net/http/internal from net/http
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
@@ -303,6 +218,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -1,155 +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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
installSystemDaemon = installSystemDaemonDarwin
|
||||
uninstallSystemDaemon = uninstallSystemDaemonDarwin
|
||||
}
|
||||
|
||||
// darwinLaunchdPlist is the launchd.plist that's written to
|
||||
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
|
||||
// future) a user-specific location.
|
||||
//
|
||||
// See man launchd.plist.
|
||||
const darwinLaunchdPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<key>Label</key>
|
||||
<string>com.tailscale.tailscaled</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/tailscaled</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
|
||||
const targetBin = "/usr/local/bin/tailscaled"
|
||||
const service = "com.tailscale.tailscaled"
|
||||
|
||||
func uninstallSystemDaemonDarwin(args []string) (ret error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("uninstall subcommand takes no arguments")
|
||||
}
|
||||
|
||||
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
|
||||
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
|
||||
running := err == nil
|
||||
|
||||
if running {
|
||||
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
|
||||
ret = err
|
||||
}
|
||||
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Remove(sysPlist); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
if err := os.Remove(targetBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func installSystemDaemonDarwin(args []string) (err error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("install subcommand takes no arguments")
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && os.Getuid() != 0 {
|
||||
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Best effort:
|
||||
uninstallSystemDaemonDarwin(nil)
|
||||
|
||||
// Copy ourselves to /usr/local/bin/tailscaled.
|
||||
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find our own executable path: %w", err)
|
||||
}
|
||||
tmpBin := targetBin + ".tmp"
|
||||
f, err := os.Create(tmpBin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Open(exe)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, self)
|
||||
self.Close()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpBin, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpBin, targetBin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/osshare"
|
||||
)
|
||||
|
||||
func init() {
|
||||
installSystemDaemon = installSystemDaemonWindows
|
||||
uninstallSystemDaemon = uninstallSystemDaemonWindows
|
||||
}
|
||||
|
||||
func installSystemDaemonWindows(args []string) (err error) {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
|
||||
}
|
||||
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err == nil {
|
||||
service.Close()
|
||||
return fmt.Errorf("service %q is already installed", serviceName)
|
||||
}
|
||||
|
||||
// no such service; proceed to install the service.
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := mgr.Config{
|
||||
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
||||
StartType: mgr.StartAutomatic,
|
||||
ErrorControl: mgr.ErrorNormal,
|
||||
DisplayName: serviceName,
|
||||
Description: "Connects this computer to others on the Tailscale network.",
|
||||
}
|
||||
|
||||
service, err = m.CreateService(serviceName, exe, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %q service: %v", serviceName, err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
// Exponential backoff is often too aggressive, so use (mostly)
|
||||
// squares instead.
|
||||
ra := []mgr.RecoveryAction{
|
||||
{mgr.ServiceRestart, 1 * time.Second},
|
||||
{mgr.ServiceRestart, 2 * time.Second},
|
||||
{mgr.ServiceRestart, 4 * time.Second},
|
||||
{mgr.ServiceRestart, 9 * time.Second},
|
||||
{mgr.ServiceRestart, 16 * time.Second},
|
||||
{mgr.ServiceRestart, 25 * time.Second},
|
||||
{mgr.ServiceRestart, 36 * time.Second},
|
||||
{mgr.ServiceRestart, 49 * time.Second},
|
||||
{mgr.ServiceRestart, 64 * time.Second},
|
||||
}
|
||||
const resetPeriodSecs = 60
|
||||
err = service.SetRecoveryActions(ra, resetPeriodSecs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set service recovery actions: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSystemDaemonWindows(args []string) (ret error) {
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q service: %v", serviceName, err)
|
||||
}
|
||||
|
||||
st, err := service.Query()
|
||||
if err != nil {
|
||||
service.Close()
|
||||
return fmt.Errorf("failed to query service state: %v", err)
|
||||
}
|
||||
if st.State != svc.Stopped {
|
||||
service.Control(svc.Stop)
|
||||
}
|
||||
err = service.Delete()
|
||||
service.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete service: %v", err)
|
||||
}
|
||||
|
||||
bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
|
||||
end := time.Now().Add(15 * time.Second)
|
||||
for time.Until(end) > 0 {
|
||||
service, err = m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
// service is no longer openable; success!
|
||||
break
|
||||
}
|
||||
service.Close()
|
||||
bo.BackOff(context.Background(), errors.New("service not deleted"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -11,11 +11,9 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
@@ -23,29 +21,30 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/ipn"
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
// startup.
|
||||
//
|
||||
// We have to support multiple state keys for other OSes (Windows in
|
||||
// particular), but right now Unix daemons run with a single
|
||||
// node-global state. To keep open the option of having per-user state
|
||||
// later, the global state key doesn't look like a username.
|
||||
const globalStateKey = "_daemon"
|
||||
|
||||
// defaultTunName returns the default tun device name for the platform.
|
||||
func defaultTunName() string {
|
||||
switch runtime.GOOS {
|
||||
@@ -53,44 +52,18 @@ func defaultTunName() string {
|
||||
return "tun"
|
||||
case "windows":
|
||||
return "Tailscale"
|
||||
case "darwin":
|
||||
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
|
||||
// as a magic value that uses/creates any free number.
|
||||
return "utun"
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology {
|
||||
// Try TUN, but fall back to userspace networking if needed.
|
||||
// See https://github.com/tailscale/tailscale-synology/issues/35
|
||||
return "tailscale0,userspace-networking"
|
||||
}
|
||||
}
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
var args struct {
|
||||
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
||||
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
||||
// or comma-separated list thereof.
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
}
|
||||
|
||||
var (
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
||||
)
|
||||
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
"install-system-daemon": &installSystemDaemon,
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -103,33 +76,18 @@ func main() {
|
||||
}
|
||||
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
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.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.tunname, "tun", defaultTunName(), "tunnel interface name")
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
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.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
sub := os.Args[1]
|
||||
if fp, ok := subCommands[sub]; ok {
|
||||
if *fp == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
|
||||
}
|
||||
if err := (*fp)(os.Args[2:]); err != nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Fatalf("fixConsoleOutput: %v", err)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
@@ -142,54 +100,24 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
if err := run(); err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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.Port = 41112
|
||||
o.StatePath = args.statepath
|
||||
o.SocketPath = args.socketpath // even for goos=="windows", for tests
|
||||
|
||||
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("tailnode.log.tailscale.io")
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
@@ -197,16 +125,6 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if isWindowsService() {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
if err := runWindowsService(pol); err != nil {
|
||||
log.Printf("runservice: %v", err)
|
||||
}
|
||||
log.Printf("Service ended.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
@@ -214,63 +132,26 @@ 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 == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
var socksListener net.Listener
|
||||
if args.socksAddr != "" {
|
||||
var err error
|
||||
socksListener, err = net.Listen("tcp", args.socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(args.socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var ns *netstack.Impl
|
||||
if useNetstack || wrapNetstack {
|
||||
onlySubnets := wrapNetstack && !useNetstack
|
||||
ns = mustStartNetstack(logf, e, onlySubnets)
|
||||
}
|
||||
|
||||
if socksListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -292,8 +173,15 @@ func run() error {
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnServerOpts()
|
||||
opts.DebugMux = debugMux
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: args.socketpath,
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
LegacyConfigPath: paths.LegacyConfigPath(),
|
||||
SurviveDisconnects: true,
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
@@ -304,86 +192,6 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, name)
|
||||
if err == nil {
|
||||
return e, useNetstack, nil
|
||||
}
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil, false, multierror.New(errs)
|
||||
}
|
||||
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
|
||||
v, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
if distro.Get() == distro.Synology {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients).
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
}
|
||||
useNetstack = name == "userspace-networking"
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
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)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, err
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if wrapNetstack {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, useNetstack, err
|
||||
}
|
||||
return e, useNetstack, nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
@@ -403,18 +211,3 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
Description=Tailscale node agent
|
||||
Documentation=https://tailscale.com/kb/
|
||||
Wants=network-pre.target
|
||||
After=network-pre.target NetworkManager.service systemd-resolved.service
|
||||
After=network-pre.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
@@ -18,7 +17,6 @@ StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import "tailscale.com/logpolicy"
|
||||
|
||||
func isWindowsService() bool { return false }
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
|
||||
|
||||
func beWindowsSubprocess() bool { return false }
|
||||
@@ -1,275 +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 "tailscale.com/cmd/tailscaled"
|
||||
|
||||
// TODO: check if administrator, like tswin does.
|
||||
//
|
||||
// TODO: try to load wintun.dll early at startup, before wireguard/tun
|
||||
// does (which panics) and if we'd fail (e.g. due to access
|
||||
// denied, even if administrator), use 'tasklist /m wintun.dll'
|
||||
// to see if something else is currently using it and tell user.
|
||||
//
|
||||
// TODO: check if Tailscale service is already running, and fail early
|
||||
// like tswin does.
|
||||
//
|
||||
// TODO: on failure, check if on a UNC drive and recommend copying it
|
||||
// to C:\ to run it, like tswin does.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
const serviceName = "Tailscale"
|
||||
|
||||
func isWindowsService() bool {
|
||||
v, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.Fatalf("svc.IsWindowsService failed: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
|
||||
type ipnService struct {
|
||||
Policy *logpolicy.Policy
|
||||
}
|
||||
|
||||
// Called by Windows to execute the windows service.
|
||||
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
args := []string{"/subproc", service.Policy.PublicID.String()}
|
||||
ipnserver.BabysitProc(ctx, args, log.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
select {
|
||||
case <-doneCh:
|
||||
case cmd := <-r:
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
case svc.Interrogate:
|
||||
changes <- cmd.CurrentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if beFirewallKillswitch() {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("stdin err (parent process died): %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(context.Background(), logid)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func beFirewallKillswitch() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
|
||||
return false
|
||||
}
|
||||
|
||||
log.SetFlags(0)
|
||||
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
|
||||
|
||||
guid, err := windows.GUIDFromString(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
|
||||
}
|
||||
|
||||
luid, err := winipcfg.LUIDFromGUID(&guid)
|
||||
if err != nil {
|
||||
log.Fatalf("no interface with GUID %q: %v", guid, err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
fw, err := wf.New(uint64(luid))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enable firewall: %v", err)
|
||||
}
|
||||
log.Printf("killswitch enabled, took %s", time.Since(start))
|
||||
|
||||
// Note(maisem): when local lan access toggled, tailscaled needs to
|
||||
// inform the firewall to let local routes through. The set of routes
|
||||
// is passed in via stdin encoded in json.
|
||||
dcd := json.NewDecoder(os.Stdin)
|
||||
for {
|
||||
var routes []netaddr.IPPrefix
|
||||
if err := dcd.Decode(&routes); err != nil {
|
||||
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
|
||||
}
|
||||
if err := fw.UpdatePermittedRoutes(routes); err != nil {
|
||||
log.Fatalf("failed to update routes (%v)", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev, nil)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("router: %w", err)
|
||||
}
|
||||
if wrapNetstack {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
onlySubnets := true
|
||||
if wrapNetstack {
|
||||
mustStartNetstack(logf, eng, onlySubnets)
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
type engineOrError struct {
|
||||
Engine wgengine.Engine
|
||||
Err error
|
||||
}
|
||||
engErrc := make(chan engineOrError)
|
||||
t0 := time.Now()
|
||||
go func() {
|
||||
const ms = time.Millisecond
|
||||
for try := 1; ; try++ {
|
||||
logf("tailscaled: getting engine... (try %v)", try)
|
||||
t1 := time.Now()
|
||||
eng, err := getEngineRaw()
|
||||
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
|
||||
if err != nil {
|
||||
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
|
||||
try, d, dt, windowsUptime().Round(time.Second), err)
|
||||
} else {
|
||||
if try > 1 {
|
||||
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
|
||||
} else {
|
||||
logf("tailscaled: got engine in %v", d)
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
engErrc <- engineOrError{eng, err}
|
||||
if err == nil {
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
<-timer.C
|
||||
}
|
||||
}()
|
||||
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
}
|
||||
for {
|
||||
res := <-engErrc
|
||||
if res.Engine != nil {
|
||||
return res.Engine, nil
|
||||
}
|
||||
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
|
||||
// Ignore errors during early boot. Windows 10 auto logs in the GUI
|
||||
// way sooner than the networking stack components start up.
|
||||
// So the network will fail for a bit (and require a few tries) while
|
||||
// the GUI is still fine.
|
||||
continue
|
||||
}
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
}
|
||||
}
|
||||
err := ipnserver.Run(ctx, logf, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
|
||||
)
|
||||
|
||||
func windowsUptime() time.Duration {
|
||||
r, _, _ := getTickCount64Proc.Call()
|
||||
return time.Duration(int64(r)) * time.Millisecond
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// The tsshd binary is an SSH server that accepts connections
|
||||
@@ -30,12 +29,10 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -60,11 +57,11 @@ func main() {
|
||||
|
||||
warned := false
|
||||
for {
|
||||
addrs, iface, err := interfaces.Tailscale()
|
||||
addr, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
log.Fatalf("listing interfaces: %v", err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
if addr == nil {
|
||||
if !warned {
|
||||
log.Printf("no tailscale interface found; polling until one is available")
|
||||
warned = true
|
||||
@@ -76,13 +73,6 @@ func main() {
|
||||
continue
|
||||
}
|
||||
warned = false
|
||||
var addr netaddr.IP
|
||||
for _, a := range addrs {
|
||||
if a.Is4() {
|
||||
addr = a
|
||||
break
|
||||
}
|
||||
}
|
||||
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
|
||||
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
|
||||
s := &ssh.Server{
|
||||
@@ -106,13 +96,7 @@ func handleSSH(s ssh.Session) {
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(tanetaddr) {
|
||||
if !interfaces.IsTailscaleIP(ta.IP) {
|
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,47 +2,111 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlclient implements the client for the Tailscale
|
||||
// control plane.
|
||||
//
|
||||
// It handles authentication, port picking, and collects the local
|
||||
// network configuration.
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
type LoginGoal struct {
|
||||
_ structs.Incomparable
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *tailcfg.Oauth2Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
loggedOutResult chan<- error
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
// TODO(apenwarr): eliminate 'state', as it's now obsolete.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (g *LoginGoal) sendLogoutError(err error) {
|
||||
if g.loggedOutResult == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case g.loggedOutResult <- err:
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
// Auto connects to a tailcontrol server for a node.
|
||||
// It's a concrete implementation of the Client interface.
|
||||
type Auto struct {
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message
|
||||
Err string
|
||||
URL string
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
State State
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
return s != nil && s2 != nil &&
|
||||
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.State == s2.State
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.State.String() + " " + string(b)
|
||||
}
|
||||
|
||||
type LoginGoal struct {
|
||||
_ structs.Incomparable
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *oauth2.Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
}
|
||||
|
||||
// Client connects to a tailcontrol server for a node.
|
||||
type Client struct {
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
logf logger.Logf
|
||||
@@ -50,21 +114,18 @@ type Auto struct {
|
||||
closed bool
|
||||
newMapCh chan struct{} // readable when we must restart a map request
|
||||
|
||||
unregisterHealthWatch func()
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -75,8 +136,8 @@ type Auto struct {
|
||||
mapDone chan struct{} // when closed, map goroutine is done
|
||||
}
|
||||
|
||||
// New creates and starts a new Auto.
|
||||
func New(opts Options) (*Auto, error) {
|
||||
// New creates and starts a new Client.
|
||||
func New(opts Options) (*Client, error) {
|
||||
c, err := NewNoStart(opts)
|
||||
if c != nil {
|
||||
c.Start()
|
||||
@@ -84,8 +145,8 @@ func New(opts Options) (*Auto, error) {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// NewNoStart creates a new Auto, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Auto, error) {
|
||||
// NewNoStart creates a new Client, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Client, error) {
|
||||
direct, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -96,7 +157,7 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
if opts.TimeNow == nil {
|
||||
opts.TimeNow = time.Now
|
||||
}
|
||||
c := &Auto{
|
||||
c := &Client{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -107,33 +168,21 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
func (c *Auto) SetPaused(paused bool) {
|
||||
func (c *Client) SetPaused(paused bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if paused == c.paused {
|
||||
return
|
||||
}
|
||||
c.logf("setPaused(%v)", paused)
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Only cancel the map routine. (The auth routine isn't expensive
|
||||
// so it's fine to keep it running.)
|
||||
// Just cancel the map routine. The auth routine isn't expensive.
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
for _, ch := range c.unpauseWaiters {
|
||||
@@ -146,56 +195,12 @@ func (c *Auto) SetPaused(paused bool) {
|
||||
// Start starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
func (c *Auto) Start() {
|
||||
func (c *Client) Start() {
|
||||
go c.authRoutine()
|
||||
go c.mapRoutine()
|
||||
}
|
||||
|
||||
// sendNewMapRequest either sends a new OmitPeers, non-streaming map request
|
||||
// (to just send Hostinfo/Netinfo/Endpoints info, while keeping an existing
|
||||
// streaming response open), or start a new streaming one if necessary.
|
||||
//
|
||||
// It should be called whenever there's something new to tell the server.
|
||||
func (c *Auto) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, send a lite update that doesn't keep a
|
||||
// long-running stream response.
|
||||
defer c.mu.Unlock()
|
||||
c.inLiteMapUpdate = true
|
||||
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
t0 := time.Now()
|
||||
err := c.direct.SendLiteMapUpdate(ctx)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
c.mu.Lock()
|
||||
c.inLiteMapUpdate = false
|
||||
c.mu.Unlock()
|
||||
if err == nil {
|
||||
c.logf("[v1] successful lite map update in %v", d)
|
||||
return
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.logf("lite map update after %v: %v", d, err)
|
||||
}
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for any reason.
|
||||
c.cancelMapSafely()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelAuth() {
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
c.authCancel()
|
||||
@@ -206,7 +211,7 @@ func (c *Auto) cancelAuth() {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapLocked() {
|
||||
func (c *Client) cancelMapLocked() {
|
||||
if c.mapCancel != nil {
|
||||
c.mapCancel()
|
||||
}
|
||||
@@ -215,17 +220,17 @@ func (c *Auto) cancelMapLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapUnsafely() {
|
||||
func (c *Client) cancelMapUnsafely() {
|
||||
c.mu.Lock()
|
||||
c.cancelMapLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapSafely() {
|
||||
func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
c.logf("cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
// received at least one netmap since the last
|
||||
@@ -247,40 +252,38 @@ func (c *Auto) cancelMapSafely() {
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("[v1] cancelMapSafely: wrote to channel")
|
||||
c.logf("cancelMapSafely: wrote to channel")
|
||||
default:
|
||||
// if channel write failed, then there was already
|
||||
// an outstanding newMapCh request. One is enough,
|
||||
// since it'll always use the latest endpoints.
|
||||
c.logf("[v1] cancelMapSafely: channel was full")
|
||||
c.logf("cancelMapSafely: channel was full")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) authRoutine() {
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
}
|
||||
synced := c.synced
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("[v1] authRoutine: quit")
|
||||
c.logf("authRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
c.logf("%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.
|
||||
@@ -290,15 +293,52 @@ func (c *Auto) authRoutine() {
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
// Wait for user to Login or Logout.
|
||||
<-ctx.Done()
|
||||
c.logf("[v1] authRoutine: context done.")
|
||||
continue
|
||||
}
|
||||
// Wait for something interesting to happen
|
||||
var exp <-chan time.Time
|
||||
var expTimer *time.Timer
|
||||
if expiry != nil && !expiry.IsZero() {
|
||||
// if expiry is in the future, don't delay
|
||||
// past that time.
|
||||
// If it's in the past, then it's already
|
||||
// being handled by someone, so no need to
|
||||
// wake ourselves up again.
|
||||
now := c.timeNow()
|
||||
if expiry.Before(now) {
|
||||
delay := expiry.Sub(now)
|
||||
if delay > 5*time.Second {
|
||||
delay = time.Second
|
||||
}
|
||||
expTimer = time.NewTimer(delay)
|
||||
exp = expTimer.C
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if expTimer != nil {
|
||||
expTimer.Stop()
|
||||
}
|
||||
c.logf("authRoutine: context done.")
|
||||
case <-exp:
|
||||
// Unfortunately the key expiry isn't provided
|
||||
// by the control server until mapRequest.
|
||||
// So we have to do some hackery with c.expiry
|
||||
// in here.
|
||||
// TODO(apenwarr): add a key expiry field in RegisterResponse.
|
||||
c.logf("authRoutine: key expiration check.")
|
||||
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
|
||||
c.logf("Key expired; setting loggedIn=false.")
|
||||
|
||||
if !goal.wantLoggedIn {
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: c.loggedIn,
|
||||
}
|
||||
c.loggedIn = false
|
||||
c.expiry = nil
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(ctx)
|
||||
goal.sendLogoutError(err)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -338,10 +378,9 @@ func (c *Auto) authRoutine() {
|
||||
report(err, f)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
} else if url != "" {
|
||||
if goal.url != "" {
|
||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||
err = fmt.Errorf("weird: server required a new url?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
|
||||
@@ -376,7 +415,7 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
// Expiry returns the credential expiration time, or the zero time if
|
||||
// the expiration time isn't known. Used in tests only.
|
||||
func (c *Auto) Expiry() *time.Time {
|
||||
func (c *Client) Expiry() *time.Time {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.expiry
|
||||
@@ -384,21 +423,21 @@ func (c *Auto) Expiry() *time.Time {
|
||||
|
||||
// Direct returns the underlying direct client object. Used in tests
|
||||
// only.
|
||||
func (c *Auto) Direct() *Direct {
|
||||
func (c *Client) Direct() *Direct {
|
||||
return c.direct
|
||||
}
|
||||
|
||||
// unpausedChanLocked returns a new channel that is closed when the
|
||||
// current Auto pause is unpaused.
|
||||
// current Client pause is unpaused.
|
||||
//
|
||||
// c.mu must be held
|
||||
func (c *Auto) unpausedChanLocked() <-chan struct{} {
|
||||
func (c *Client) unpausedChanLocked() <-chan struct{} {
|
||||
unpaused := make(chan struct{})
|
||||
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
|
||||
return unpaused
|
||||
}
|
||||
|
||||
func (c *Auto) mapRoutine() {
|
||||
func (c *Client) mapRoutine() {
|
||||
defer close(c.mapDone)
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
|
||||
|
||||
@@ -430,7 +469,7 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
c.logf("%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.
|
||||
@@ -459,15 +498,13 @@ func (c *Auto) mapRoutine() {
|
||||
c.mu.Lock()
|
||||
c.inPollNetMap = false
|
||||
c.mu.Unlock()
|
||||
health.SetInPollNetMap(false)
|
||||
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
|
||||
health.SetInPollNetMap(true)
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
|
||||
c.mu.Lock()
|
||||
|
||||
select {
|
||||
case <-c.newMapCh:
|
||||
c.logf("[v1] mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.cancelMapLocked()
|
||||
|
||||
// Don't emit this netmap; we're
|
||||
@@ -489,13 +526,12 @@ func (c *Auto) mapRoutine() {
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] mapRoutine: netmap received: %s", state)
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
})
|
||||
|
||||
health.SetInPollNetMap(false)
|
||||
c.mu.Lock()
|
||||
c.synced = false
|
||||
c.inPollNetMap = false
|
||||
@@ -520,24 +556,20 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) AuthCantContinue() bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
func (c *Client) AuthCantContinue() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return !c.loggedIn && (c.loginGoal == nil || c.loginGoal.url != "")
|
||||
}
|
||||
|
||||
// SetStatusFunc sets fn as the callback to run on any status change.
|
||||
func (c *Auto) SetStatusFunc(fn func(Status)) {
|
||||
func (c *Client) SetStatusFunc(fn func(Status)) {
|
||||
c.mu.Lock()
|
||||
c.statusFunc = fn
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
@@ -545,25 +577,27 @@ func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
// No changes. Don't log.
|
||||
return
|
||||
}
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
// Send new Hostinfo to server
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
if ni == nil {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
if !c.direct.SetNetInfo(ni) {
|
||||
c.logf("[unexpected] duplicate NetInfo: %v", ni)
|
||||
return
|
||||
}
|
||||
c.logf("NetInfo: %v", ni)
|
||||
|
||||
// Send new Hostinfo (which includes NetInfo) to server
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.mu.Lock()
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
@@ -573,15 +607,12 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
c.logf("sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *persist.Persist
|
||||
var loginFin, logoutFin *empty.Message
|
||||
var p *Persist
|
||||
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()
|
||||
@@ -592,13 +623,12 @@ 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,
|
||||
LoginFinished: fin,
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
State: state,
|
||||
}
|
||||
if err != nil {
|
||||
new.Err = err.Error()
|
||||
@@ -612,7 +642,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
func (c *Client) Login(t *oauth2.Token, flags LoginFlags) {
|
||||
c.logf("client.Login(%v, %v)", t != nil, flags)
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -626,57 +656,26 @@ func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
c.cancelAuth()
|
||||
}
|
||||
|
||||
func (c *Auto) StartLogout() {
|
||||
c.logf("client.StartLogout()")
|
||||
func (c *Client) Logout() {
|
||||
c.logf("client.Logout()")
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: false,
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.cancelAuth()
|
||||
}
|
||||
|
||||
func (c *Auto) Logout(ctx context.Context) error {
|
||||
c.logf("client.Logout()")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: false,
|
||||
loggedOutResult: errc,
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.cancelAuth()
|
||||
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateEndpoints sets the client's discovered endpoints and sends
|
||||
// them to the control server if they've changed.
|
||||
//
|
||||
// It does not retain the provided slice.
|
||||
//
|
||||
// The localPort field is unused except for integration tests in
|
||||
// another repo.
|
||||
func (c *Auto) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
|
||||
changed := c.direct.SetEndpoints(localPort, endpoints)
|
||||
if changed {
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) Shutdown() {
|
||||
func (c *Client) Shutdown() {
|
||||
c.logf("client.Shutdown()")
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -690,7 +689,6 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
|
||||
if !closed {
|
||||
c.unregisterHealthWatch()
|
||||
close(c.quit)
|
||||
c.cancelAuth()
|
||||
<-c.authDone
|
||||
@@ -702,23 +700,17 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
|
||||
func (c *Client) TestOnlyNodePublicKey() wgcfg.Key {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
||||
func (c *Client) TestOnlySetAuthKey(authkey string) {
|
||||
c.direct.mu.Lock()
|
||||
defer c.direct.mu.Unlock()
|
||||
c.direct.authKey = authkey
|
||||
}
|
||||
|
||||
func (c *Auto) TestOnlyTimeNow() time.Time {
|
||||
func (c *Client) 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)
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlclient implements the client for the Tailscale
|
||||
// control plane.
|
||||
//
|
||||
// It handles authentication, port picking, and collects the local
|
||||
// network configuration.
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
type LoginFlags int
|
||||
|
||||
const (
|
||||
LoginDefault = LoginFlags(0)
|
||||
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
|
||||
)
|
||||
|
||||
// Client represents a client connection to the control server.
|
||||
// Currently this is done through a pair of polling https requests in
|
||||
// the Auto client, but that might change eventually.
|
||||
type Client interface {
|
||||
// SetStatusFunc provides a callback to call when control sends us
|
||||
// a message.
|
||||
SetStatusFunc(func(Status))
|
||||
// Shutdown closes this session, which should not be used any further
|
||||
// afterwards.
|
||||
Shutdown()
|
||||
// Login begins an interactive or non-interactive login process.
|
||||
// Client will eventually call the Status callback with either a
|
||||
// LoginFinished flag (on success) or an auth URL (if further
|
||||
// interaction is needed).
|
||||
Login(*tailcfg.Oauth2Token, LoginFlags)
|
||||
// StartLogout starts an asynchronous logout process.
|
||||
// When it finishes, the Status callback will be called while
|
||||
// AuthCantContinue()==true.
|
||||
StartLogout()
|
||||
// Logout starts a synchronous logout process. It doesn't return
|
||||
// until the logout operation has been completed.
|
||||
Logout(context.Context) error
|
||||
// SetPaused pauses or unpauses the controlclient activity as much
|
||||
// as possible, without losing its internal state, to minimize
|
||||
// unnecessary network activity.
|
||||
// TODO: It might be better to simply shutdown the controlclient and
|
||||
// make a new one when it's time to unpause.
|
||||
SetPaused(bool)
|
||||
// AuthCantContinue returns whether authentication is blocked. If it
|
||||
// is, you either need to visit the auth URL (previously sent in a
|
||||
// Status callback) or call the Login function appropriately.
|
||||
// TODO: this probably belongs in the Status itself instead.
|
||||
AuthCantContinue() bool
|
||||
// SetHostinfo changes the Hostinfo structure that will be sent in
|
||||
// subsequent node registration requests.
|
||||
// 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.
|
||||
SetHostinfo(*tailcfg.Hostinfo)
|
||||
// SetNetinfo changes the NetIinfo structure that will be sent in
|
||||
// subsequent node registration requests.
|
||||
// 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.
|
||||
SetNetInfo(*tailcfg.NetInfo)
|
||||
// UpdateEndpoints changes the Endpoint structure that will be sent
|
||||
// in subsequent node registration requests.
|
||||
// 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
|
||||
}
|
||||
@@ -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", "Persist", "NetMap", "Hostinfo", "State"}
|
||||
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)
|
||||
@@ -42,11 +42,6 @@ func TestStatusEqual(t *testing.T) {
|
||||
&Status{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Status{},
|
||||
&Status{},
|
||||
|
||||
@@ -1,122 +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 controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d)
|
||||
} else {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, 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'
|
||||
}
|
||||
@@ -1,30 +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 controlclient
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
20
control/controlclient/direct_clone.go
Normal file
20
control/controlclient/direct_clone.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
|
||||
|
||||
package controlclient
|
||||
|
||||
import ()
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
@@ -5,101 +5,89 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
|
||||
return &tailcfg.Node{ID: id, Name: name}
|
||||
}
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.serverURL != opts.ServerURL {
|
||||
t.Errorf("c.serverURL got %v want %v", c.serverURL, opts.ServerURL)
|
||||
}
|
||||
|
||||
if !hi.Equal(c.hostinfo) {
|
||||
t.Errorf("c.hostinfo got %v want %v", c.hostinfo, hi)
|
||||
}
|
||||
|
||||
changed := c.SetNetInfo(&ni)
|
||||
if changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want false got %v", changed)
|
||||
}
|
||||
ni = tailcfg.NetInfo{LinkType: "wifi"}
|
||||
changed = c.SetNetInfo(&ni)
|
||||
if !changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want true got %v", changed)
|
||||
}
|
||||
|
||||
changed = c.SetHostinfo(hi)
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
|
||||
}
|
||||
|
||||
endpoints := fakeEndpoints(1, 2, 3)
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(12) want true got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if changed {
|
||||
t.Errorf("c.newEndpoints(12) want false got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
endpoints = fakeEndpoints(4, 5, 6)
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
}
|
||||
|
||||
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
for _, port := range ports {
|
||||
ret = append(ret, tailcfg.Endpoint{
|
||||
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
20
control/controlclient/filter.go
Normal file
20
control/controlclient/filter.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
@@ -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 linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package controlclient
|
||||
@@ -10,11 +9,12 @@ package controlclient
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/hostinfo"
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -55,12 +55,9 @@ func osVersionLinux() string {
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
if hostinfo.InContainer() {
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
if env := hostinfo.GetEnvType(); env != "" {
|
||||
fmt.Fprintf(&attrBuf, "; env=%s", env)
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
|
||||
id := m["ID"]
|
||||
@@ -76,7 +73,7 @@ func osVersionLinux() string {
|
||||
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
|
||||
}
|
||||
fallthrough
|
||||
case "fedora", "rhel", "alpine", "nixos":
|
||||
case "fedora", "rhel", "alpine":
|
||||
// Their PRETTY_NAME is fine as-is for all versions I tested.
|
||||
fallthrough
|
||||
default:
|
||||
@@ -92,3 +89,15 @@ 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
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package controlclient
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -15,12 +14,7 @@ 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"
|
||||
@@ -32,8 +26,5 @@ func osVersionWindows() string {
|
||||
if sp := strings.Index(s, " "); sp != -1 {
|
||||
s = s[sp+1:]
|
||||
}
|
||||
if s != "" {
|
||||
winVerCache.Store(s)
|
||||
}
|
||||
return s // "10.0.19041.388", ideally
|
||||
}
|
||||
|
||||
@@ -1,301 +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 controlclient
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// mapSession holds the state over a long-polled "map" request to the
|
||||
// control plane.
|
||||
//
|
||||
// It accepts incremental tailcfg.MapResponse values to
|
||||
// netMapForResponse and returns fully inflated NetworkMaps, filling
|
||||
// in the omitted data implicit from prior MapResponse values from
|
||||
// within the same session (the same long-poll HTTP response to the
|
||||
// one MapRequest).
|
||||
type mapSession struct {
|
||||
// Immutable fields.
|
||||
privateNodeKey wgkey.Private
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey tailcfg.MachineKey
|
||||
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
|
||||
|
||||
// Fields storing state over the the coards of multiple MapResponses.
|
||||
lastNode *tailcfg.Node
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastParsedPacketFilter []filter.Match
|
||||
collectServices bool
|
||||
previousPeers []*tailcfg.Node // for delta-purposes
|
||||
lastDomain string
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
netMapBuilding *netmap.NetworkMap
|
||||
}
|
||||
|
||||
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
|
||||
ms := &mapSession{
|
||||
privateNodeKey: privateNodeKey,
|
||||
logf: logger.Discard,
|
||||
vlogf: logger.Discard,
|
||||
lastDNSConfig: new(tailcfg.DNSConfig),
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *mapSession) addUserProfile(userID tailcfg.UserID) {
|
||||
nm := ms.netMapBuilding
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
// Already populated it from a previous peer.
|
||||
return
|
||||
}
|
||||
if up, ok := ms.lastUserProfile[userID]; ok {
|
||||
nm.UserProfiles[userID] = up
|
||||
}
|
||||
}
|
||||
|
||||
// netmapForResponse returns a fully populated NetworkMap from a full
|
||||
// or incremental MapResponse within the session, filling in omitted
|
||||
// information from prior MapResponse values.
|
||||
func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.NetworkMap {
|
||||
undeltaPeers(resp, ms.previousPeers)
|
||||
|
||||
ms.previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
for _, up := range resp.UserProfiles {
|
||||
ms.lastUserProfile[up.ID] = up
|
||||
}
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
ms.vlogf("netmap: new map contains DERP map")
|
||||
ms.lastDERPMap = resp.DERPMap
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
var err error
|
||||
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
ms.logf("parsePacketFilter: %v", err)
|
||||
}
|
||||
}
|
||||
if c := resp.DNSConfig; c != nil {
|
||||
ms.lastDNSConfig = c
|
||||
}
|
||||
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
ms.collectServices = v
|
||||
}
|
||||
if resp.Domain != "" {
|
||||
ms.lastDomain = resp.Domain
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
if resp.Node != nil {
|
||||
ms.lastNode = resp.Node
|
||||
}
|
||||
if node := ms.lastNode.Clone(); node != nil {
|
||||
nm.SelfNode = node
|
||||
nm.Expiry = node.KeyExpiry
|
||||
nm.Name = node.Name
|
||||
nm.Addresses = filterSelfAddresses(node.Addresses)
|
||||
nm.User = node.User
|
||||
nm.Hostinfo = node.Hostinfo
|
||||
if node.MachineAuthorized {
|
||||
nm.MachineStatus = tailcfg.MachineAuthorized
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
ms.addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
if nm.SelfNode != nil {
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
}
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if ms.keepSharerAndUserSplit {
|
||||
ms.addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
ms.netMapBuilding = nil
|
||||
return nm
|
||||
}
|
||||
|
||||
// undeltaPeers updates mapRes.Peers to be complete based on the
|
||||
// provided previous peer list and the PeersRemoved and PeersChanged
|
||||
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
|
||||
// maps.
|
||||
//
|
||||
// It then also nils out the delta fields.
|
||||
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if len(mapRes.Peers) > 0 {
|
||||
// Not delta encoded.
|
||||
if !nodesSorted(mapRes.Peers) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
||||
sortNodes(mapRes.Peers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var removed map[tailcfg.NodeID]bool
|
||||
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
||||
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
||||
for _, id := range pr {
|
||||
removed[id] = true
|
||||
}
|
||||
}
|
||||
changed := mapRes.PeersChanged
|
||||
|
||||
if !nodesSorted(changed) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
||||
sortNodes(changed)
|
||||
}
|
||||
if !nodesSorted(prev) {
|
||||
// Internal error (unrelated to the network) if we get here.
|
||||
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
||||
sortNodes(prev)
|
||||
}
|
||||
|
||||
newFull := prev
|
||||
if len(removed) > 0 || len(changed) > 0 {
|
||||
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
||||
for len(prev) > 0 && len(changed) > 0 {
|
||||
pID := prev[0].ID
|
||||
cID := changed[0].ID
|
||||
if removed[pID] {
|
||||
prev = prev[1:]
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case pID < cID:
|
||||
newFull = append(newFull, prev[0])
|
||||
prev = prev[1:]
|
||||
case pID == cID:
|
||||
newFull = append(newFull, changed[0])
|
||||
prev, changed = prev[1:], changed[1:]
|
||||
case cID < pID:
|
||||
newFull = append(newFull, changed[0])
|
||||
changed = changed[1:]
|
||||
}
|
||||
}
|
||||
newFull = append(newFull, changed...)
|
||||
for _, n := range prev {
|
||||
if !removed[n.ID] {
|
||||
newFull = append(newFull, n)
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
}
|
||||
|
||||
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := clockNow()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for nodeID, online := range mapRes.OnlineChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
online := online
|
||||
n.Online = &online
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
}
|
||||
|
||||
func nodesSorted(v []*tailcfg.Node) bool {
|
||||
for i, n := range v {
|
||||
if i > 0 && n.ID <= v[i-1].ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sortNodes(v []*tailcfg.Node) {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
||||
}
|
||||
|
||||
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
if v1 == nil {
|
||||
return nil
|
||||
}
|
||||
v2 := make([]*tailcfg.Node, len(v1))
|
||||
for i, n := range v1 {
|
||||
v2[i] = n.Clone()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,311 +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 controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
defer func(old func() time.Time) { clockNow = old }(clockNow)
|
||||
|
||||
var curTime time.Time
|
||||
clockNow = func() time.Time {
|
||||
return curTime
|
||||
}
|
||||
online := func(v bool) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Online = &v
|
||||
}
|
||||
}
|
||||
seenAt := func(t time.Time) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.LastSeen = &t
|
||||
}
|
||||
}
|
||||
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name}
|
||||
for _, f := range mod {
|
||||
f(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
curTime time.Time
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "online_change",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(true)),
|
||||
n(2, "bar"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "online_change_offline",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(false)),
|
||||
n(2, "bar", online(true)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "peer_seen_at",
|
||||
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
|
||||
curTime: time.Unix(123, 0),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeerSeenChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo"),
|
||||
n(2, "bar", seenAt(time.Unix(123, 0))),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.curTime.IsZero() {
|
||||
curTime = tt.curTime
|
||||
}
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
var extra string
|
||||
if n.Online != nil {
|
||||
extra += fmt.Sprintf(", online=%v", *n.Online)
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func newTestMapSession(t *testing.T) *mapSession {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newMapSession(k)
|
||||
}
|
||||
|
||||
func TestNetmapForResponse(t *testing.T) {
|
||||
t.Run("implicit_packetfilter", func(t *testing.T) {
|
||||
somePacketFilter := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ms := newTestMapSession(t)
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
PacketFilter: somePacketFilter,
|
||||
})
|
||||
if len(nm1.PacketFilter) == 0 {
|
||||
t.Fatalf("zero length PacketFilter")
|
||||
}
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
PacketFilter: nil, // testing that the server can omit this.
|
||||
})
|
||||
if len(nm1.PacketFilter) == 0 {
|
||||
t.Fatalf("zero length PacketFilter in 2nd netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm1.PacketFilter, nm2.PacketFilter) {
|
||||
t.Error("packet filters differ")
|
||||
}
|
||||
})
|
||||
t.Run("implicit_dnsconfig", func(t *testing.T) {
|
||||
someDNSConfig := &tailcfg.DNSConfig{Domains: []string{"foo", "bar"}}
|
||||
ms := newTestMapSession(t)
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
DNSConfig: someDNSConfig,
|
||||
})
|
||||
if !reflect.DeepEqual(nm1.DNS, *someDNSConfig) {
|
||||
t.Fatalf("1st DNS wrong")
|
||||
}
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
DNSConfig: nil, // implict
|
||||
})
|
||||
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
|
||||
t.Fatalf("2nd DNS wrong")
|
||||
}
|
||||
})
|
||||
t.Run("collect_services", func(t *testing.T) {
|
||||
ms := newTestMapSession(t)
|
||||
var nm *netmap.NetworkMap
|
||||
wantCollect := func(v bool) {
|
||||
t.Helper()
|
||||
if nm.CollectServices != v {
|
||||
t.Errorf("netmap.CollectServices = %v; want %v", nm.CollectServices, v)
|
||||
}
|
||||
}
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
})
|
||||
wantCollect(false)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "false",
|
||||
})
|
||||
wantCollect(false)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "true",
|
||||
})
|
||||
wantCollect(true)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "",
|
||||
})
|
||||
wantCollect(true)
|
||||
})
|
||||
t.Run("implicit_domain", func(t *testing.T) {
|
||||
ms := newTestMapSession(t)
|
||||
var nm *netmap.NetworkMap
|
||||
want := func(v string) {
|
||||
t.Helper()
|
||||
if nm.Domain != v {
|
||||
t.Errorf("netmap.Domain = %q; want %q", nm.Domain, v)
|
||||
}
|
||||
}
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
Domain: "foo.com",
|
||||
})
|
||||
want("foo.com")
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
})
|
||||
want("foo.com")
|
||||
})
|
||||
t.Run("implicit_node", func(t *testing.T) {
|
||||
someNode := &tailcfg.Node{
|
||||
Name: "foo",
|
||||
}
|
||||
wantNode := &tailcfg.Node{
|
||||
Name: "foo",
|
||||
ComputedName: "foo",
|
||||
ComputedNameWithHost: "foo",
|
||||
}
|
||||
ms := newTestMapSession(t)
|
||||
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: someNode,
|
||||
})
|
||||
if nm1.SelfNode == nil {
|
||||
t.Fatal("nil Node in 1st netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm1.SelfNode, wantNode) {
|
||||
j, _ := json.Marshal(nm1.SelfNode)
|
||||
t.Errorf("Node mismatch in 1st netmap; got: %s", j)
|
||||
}
|
||||
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{})
|
||||
if nm2.SelfNode == nil {
|
||||
t.Fatal("nil Node in 1st netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm2.SelfNode, wantNode) {
|
||||
j, _ := json.Marshal(nm2.SelfNode)
|
||||
t.Errorf("Node mismatch in 2nd netmap; got: %s", j)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,49 +2,39 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package netmap contains the netmap.NetworkMap type.
|
||||
package netmap
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// NetworkMap is the current state of the world.
|
||||
//
|
||||
// The fields should all be considered read-only. They might
|
||||
// alias parts of previous NetworkMap values.
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
SelfNode *tailcfg.Node
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []netaddr.IPPrefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly)
|
||||
LocalPort uint16 // used for debugging
|
||||
Addresses []wgcfg.CIDR
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS tailcfg.DNSConfig
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
PacketFilter []filter.Match
|
||||
|
||||
// CollectServices reports whether this node's Tailnet has
|
||||
// requested that info about services be included in HostInfo.
|
||||
// If set, Hostinfo.ShieldsUp blocks services collection; that
|
||||
// takes precedence over this field.
|
||||
CollectServices bool
|
||||
PacketFilter filter.Matches
|
||||
|
||||
// DERPMap is the last DERP server map received. It's reused
|
||||
// between updates and should not be modified.
|
||||
@@ -57,23 +47,14 @@ type NetworkMap struct {
|
||||
|
||||
User tailcfg.UserID
|
||||
Domain string
|
||||
|
||||
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
|
||||
// There are lots of ways to slice this data, leave it up to users.
|
||||
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
|
||||
// TODO(crawshaw): Groups []tailcfg.Group
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
|
||||
// MagicDNS isn't necessarily in use).
|
||||
//
|
||||
// It will neither start nor end with a period.
|
||||
func (nm *NetworkMap) MagicDNSSuffix() string {
|
||||
name := strings.Trim(nm.Name, ".")
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[i+1:]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) String() string {
|
||||
func (nm NetworkMap) String() string {
|
||||
return nm.Concise()
|
||||
}
|
||||
|
||||
@@ -247,8 +228,113 @@ type WGConfigFlags int
|
||||
const (
|
||||
AllowSingleHosts WGConfigFlags = 1 << iota
|
||||
AllowSubnetRoutes
|
||||
AllowDefaultRoute
|
||||
)
|
||||
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
|
||||
// This form is then recognize by magicsock's CreateEndpoint.
|
||||
const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
|
||||
// WGCfg returns the NetworkMaps's Wireguard configuration.
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: nm.PrivateKey,
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
|
||||
for _, peer := range nm.Peers {
|
||||
if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
|
||||
logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
|
||||
PublicKey: wgcfg.Key(peer.Key),
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
if peer.KeepAlive {
|
||||
cpeer.PersistentKeepalive = 25 // seconds
|
||||
}
|
||||
|
||||
if !peer.DiscoKey.IsZero() {
|
||||
if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpeer.Endpoints = []wgcfg.Endpoint{{Host: fmt.Sprintf("%x.disco.tailscale", peer.DiscoKey[:]), Port: 12345}}
|
||||
} else {
|
||||
if err := appendEndpoint(cpeer, peer.DERP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ep := range peer.Endpoints {
|
||||
if err := appendEndpoint(cpeer, ep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Mask == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr wgcfg.CIDR) bool {
|
||||
if cidr.Mask == 0 {
|
||||
return false
|
||||
}
|
||||
if cidr.Mask < 32 {
|
||||
// Fast path for IPv4, to avoid loop below.
|
||||
//
|
||||
// TODO: if cidr.IP is an IPv6 address, we could do "< 128"
|
||||
// to avoid the range over node.Addresses. Or we could
|
||||
// just remove this fast path and unconditionally do the range
|
||||
// loop.
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
if cidr == selfCIDR {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
|
||||
if epStr == "" {
|
||||
return nil
|
||||
}
|
||||
host, port, err := net.SplitHostPort(epStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
peer.Endpoints = append(peer.Endpoints, wgcfg.Endpoint{Host: host, Port: uint16(port16)})
|
||||
return nil
|
||||
}
|
||||
|
||||
// eqStringsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqStringsIgnoreNil(a, b []string) bool {
|
||||
@@ -265,7 +351,7 @@ func eqStringsIgnoreNil(a, b []string) bool {
|
||||
|
||||
// eqCIDRsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqCIDRsIgnoreNil(a, b []netaddr.IPPrefix) bool {
|
||||
func eqCIDRsIgnoreNil(a, b []wgcfg.CIDR) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netmap
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -250,7 +250,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2,24 +2,15 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package persist
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/wgkey"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if name := t.Field(i).Name; name != "_" {
|
||||
fields = append(fields, name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
||||
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
|
||||
@@ -27,8 +18,8 @@ func TestPersistEqual(t *testing.T) {
|
||||
have, persistHandles)
|
||||
}
|
||||
|
||||
newPrivate := func() wgkey.Private {
|
||||
k, err := wgkey.NewPrivate()
|
||||
newPrivate := func() wgcfg.PrivateKey {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoCertStore = errors.New("no certificate store")
|
||||
errCertificateNotConfigured = errors.New("no certificate subject configured")
|
||||
)
|
||||
|
||||
// HashRegisterRequest generates the hash required sign or verify a
|
||||
// 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.
|
||||
fmt.Fprintf(h, "%s%s%s%s%s",
|
||||
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && cgo
|
||||
// +build windows,cgo
|
||||
|
||||
// darwin,cgo is also supported by certstore but machineCertificateSubject will
|
||||
// need to be loaded by a different mechanism, so this is not currently enabled
|
||||
// on darwin.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
var getMachineCertificateSubjectOnce struct {
|
||||
sync.Once
|
||||
v string // Subject of machine certificate to search for
|
||||
}
|
||||
|
||||
// getMachineCertificateSubject returns the exact name of a Subject that needs
|
||||
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
||||
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
||||
// itself, an intermediate CA or the root CA.
|
||||
//
|
||||
// If getMachineCertificateSubject() returns "" then no lookup will occur and
|
||||
// each RegisterRequest will be unsigned.
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
func getMachineCertificateSubject() string {
|
||||
getMachineCertificateSubjectOnce.Do(func() {
|
||||
getMachineCertificateSubjectOnce.v = winutil.GetRegString("MachineCertificateSubject", "")
|
||||
})
|
||||
|
||||
return getMachineCertificateSubjectOnce.v
|
||||
}
|
||||
|
||||
var (
|
||||
errNoMatch = errors.New("no matching certificate")
|
||||
errBadRequest = errors.New("malformed request")
|
||||
)
|
||||
|
||||
func isSupportedCertificate(cert *x509.Certificate) bool {
|
||||
return cert.PublicKeyAlgorithm == x509.RSA
|
||||
}
|
||||
|
||||
func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
|
||||
if len(chain) == 0 || chain[0] == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range chain {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
if c.Subject.String() == subject {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func selectIdentityFromSlice(subject string, ids []certstore.Identity) (certstore.Identity, []*x509.Certificate) {
|
||||
for _, id := range ids {
|
||||
chain, err := id.CertificateChain()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isSupportedCertificate(chain[0]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isSubjectInChain(subject, chain) {
|
||||
return id, chain
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// findIdentity locates an identity from the Windows or Darwin certificate
|
||||
// store. It returns the first certificate with a matching Subject anywhere in
|
||||
// its certificate chain, so it is possible to search for the leaf certificate,
|
||||
// intermediate CA or root CA. If err is nil then the returned identity will
|
||||
// never be nil (if no identity is found, the error errNoMatch will be
|
||||
// returned). If an identity is returned then its certificate chain is also
|
||||
// returned.
|
||||
func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x509.Certificate, error) {
|
||||
ids, err := st.Identities()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
selected, chain := selectIdentityFromSlice(subject, ids)
|
||||
|
||||
for _, id := range ids {
|
||||
if id != selected {
|
||||
id.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
return nil, nil, errNoMatch
|
||||
}
|
||||
|
||||
return selected, chain, nil
|
||||
}
|
||||
|
||||
// signRegisterRequest looks for a suitable machine identity from the local
|
||||
// system certificate store, and if one is found, signs the RegisterRequest
|
||||
// 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 wgkey.Key) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signRegisterRequest: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if req.Timestamp == nil {
|
||||
return errBadRequest
|
||||
}
|
||||
|
||||
machineCertificateSubject := getMachineCertificateSubject()
|
||||
if machineCertificateSubject == "" {
|
||||
return errCertificateNotConfigured
|
||||
}
|
||||
|
||||
st, err := certstore.Open(certstore.System)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open cert store: %w", err)
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
id, chain, err := findIdentity(machineCertificateSubject, st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find identity: %w", err)
|
||||
}
|
||||
defer id.Close()
|
||||
|
||||
signer, err := id.Signer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create signer: %w", err)
|
||||
}
|
||||
|
||||
cl := 0
|
||||
for _, c := range chain {
|
||||
cl += len(c.Raw)
|
||||
}
|
||||
req.DeviceCert = make([]byte, 0, cl)
|
||||
for _, c := range chain {
|
||||
req.DeviceCert = append(req.DeviceCert, c.Raw...)
|
||||
}
|
||||
|
||||
h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
|
||||
|
||||
req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
Hash: crypto.SHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: %w", err)
|
||||
}
|
||||
req.SignatureType = tailcfg.SignatureV1
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows || !cgo
|
||||
// +build !windows !cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// signRegisterRequest on non-supported platforms always returns errNoCertStore.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error {
|
||||
return errNoCertStore
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
//
|
||||
// TODO(apenwarr): eliminate the state, as it's now obsolete.
|
||||
//
|
||||
// apenwarr: Historical note: controlclient.Auto was originally
|
||||
// intended to be the state machine for the whole tailscale client, but that
|
||||
// turned out to not be the right abstraction layer, and it moved to
|
||||
// ipn.Backend. Since ipn.Backend now has a state machine, it would be
|
||||
// much better if controlclient could be a simple stateless API. But the
|
||||
// current server-side API (two interlocking polling https calls) makes that
|
||||
// very hard to implement. A server side API change could untangle this and
|
||||
// remove all the statefulness.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
LogoutFinished *empty.Message // nonempty when logout 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
|
||||
// use them. Please don't use these fields.
|
||||
// TODO(apenwarr): Unexport or remove these.
|
||||
State State
|
||||
Persist *persist.Persist // locally persisted configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
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) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.State == s2.State
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.State.String() + " " + string(b)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -59,8 +59,7 @@ Login:
|
||||
* server sends frameServerInfo
|
||||
|
||||
Steady state:
|
||||
* server occasionally sends frameKeepAlive (or framePing)
|
||||
* client responds to any framePing with a framePong
|
||||
* server occasionally sends frameKeepAlive
|
||||
* client sends frameSendPacket
|
||||
* server then sends frameRecvPacket to recipient
|
||||
*/
|
||||
@@ -98,9 +97,6 @@ const (
|
||||
// connection. (To be used for cluster load balancing
|
||||
// purposes, when clients end up on a non-ideal node)
|
||||
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
|
||||
|
||||
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -21,15 +21,13 @@ import (
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
canAckPings bool
|
||||
isProber bool
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -50,10 +48,8 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -62,22 +58,12 @@ type clientOpt struct {
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
|
||||
// IsProber returns a ClientOpt to pass to the DERP server during connect to
|
||||
// declare that this client is a a prober.
|
||||
func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.IsProber = v }) }
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
// CanAckPings returns a ClientOpt to set whether it advertises to the
|
||||
// server that it's capable of acknowledging ping requests.
|
||||
func CanAckPings(v bool) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
@@ -91,15 +77,13 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
canAckPings: opt.CanAckPings,
|
||||
isProber: opt.IsProber,
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
}
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
@@ -163,13 +147,6 @@ type clientInfo struct {
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
|
||||
// CanAckPings is whether the client declares it's able to ack
|
||||
// pings.
|
||||
CanAckPings bool
|
||||
|
||||
// IsProber is whether this client is a prober.
|
||||
IsProber bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -178,10 +155,8 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
IsProber: c.isProber,
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -263,18 +238,6 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
// NotePreferred sends a packet that tells the server whether this
|
||||
// client is the user's preferred server. This is only used in the
|
||||
// server for stats.
|
||||
@@ -356,19 +319,6 @@ type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// PingMessage is a request from a client or server to reply to the
|
||||
// other side with a PongMessage with the given payload.
|
||||
type PingMessage [8]byte
|
||||
|
||||
func (PingMessage) msg() {}
|
||||
|
||||
// KeepAliveMessage is a one-way empty message from server to client, just to
|
||||
// keep the connection alive. It's like a PingMessage, but doesn't solicit
|
||||
// a reply from the client.
|
||||
type KeepAliveMessage struct{}
|
||||
|
||||
func (KeepAliveMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -447,9 +397,9 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
|
||||
return ServerInfoMessage{}, nil
|
||||
case frameKeepAlive:
|
||||
// A one-way keep-alive message that doesn't require an acknowledgement.
|
||||
// This predated framePing/framePong.
|
||||
return KeepAliveMessage{}, nil
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
continue
|
||||
case framePeerGone:
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
@@ -477,15 +427,6 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
|
||||
case framePing:
|
||||
var pm PingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
continue
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,30 +20,22 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/pad32"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -77,6 +69,13 @@ const (
|
||||
writeTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
const host64bit = (^uint(0) >> 32) & 1 // 1 on 64-bit, 0 on 32-bit
|
||||
|
||||
// pad32bit is 4 on 32-bit machines and 0 on 64-bit.
|
||||
// It exists so the Server struct's atomic fields can be aligned to 8
|
||||
// byte boundaries. (As tested by GOARCH=386 go test, etc)
|
||||
const pad32bit = 4 - host64bit*4 // 0 on 64-bit, 4 on 32-bit
|
||||
|
||||
// Server is a DERP server.
|
||||
type Server struct {
|
||||
// WriteTimeout, if non-zero, specifies how long to wait
|
||||
@@ -92,46 +91,42 @@ type Server struct {
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
|
||||
// Counters:
|
||||
_ pad32.Four
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ pad32.Four
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
|
||||
packetsDroppedType metrics.LabelMap
|
||||
packetsDroppedTypeDisco *expvar.Int
|
||||
packetsDroppedTypeOther *expvar.Int
|
||||
_ pad32.Four
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
clientsReplaced expvar.Int
|
||||
clientsReplaceLimited expvar.Int
|
||||
clientsReplaceSleeping expvar.Int
|
||||
unknownFrames expvar.Int
|
||||
homeMovesIn expvar.Int // established clients announce home server moves in
|
||||
homeMovesOut expvar.Int // established clients announce home server moves out
|
||||
multiForwarderCreated expvar.Int
|
||||
multiForwarderDeleted expvar.Int
|
||||
removePktForwardOther expvar.Int
|
||||
avgQueueDuration *uint64 // In milliseconds; accessed atomically
|
||||
_ [pad32bit]byte
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedUnknown *expvar.Int // unknown dst pubkey
|
||||
packetsDroppedFwdUnknown *expvar.Int // unknown dst pubkey on forward
|
||||
packetsDroppedGone *expvar.Int // dst conn shutting down
|
||||
packetsDroppedQueueHead *expvar.Int // queue full, drop head packet
|
||||
packetsDroppedQueueTail *expvar.Int // queue full, drop tail packet
|
||||
packetsDroppedWrite *expvar.Int // error writing to dst conn
|
||||
_ [pad32bit]byte
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
clientsReplaced expvar.Int
|
||||
unknownFrames expvar.Int
|
||||
homeMovesIn expvar.Int // established clients announce home server moves in
|
||||
homeMovesOut expvar.Int // established clients announce home server moves out
|
||||
multiForwarderCreated expvar.Int
|
||||
multiForwarderDeleted expvar.Int
|
||||
removePktForwardOther expvar.Int
|
||||
|
||||
// verifyClients only accepts client connections to the DERP server if the clientKey is a
|
||||
// known peer in the network, as specified by a running tailscaled's client's local api.
|
||||
verifyClients bool
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]*sclient
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]*sclient
|
||||
clientsEver map[key.Public]bool // never deleted from, for stats; fine for now
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
// clientsMesh tracks all clients in the cluster, both locally
|
||||
// and to mesh peers. If the value is nil, that means the
|
||||
// peer is only local (and thus in the clients Map, but not
|
||||
@@ -143,9 +138,6 @@ type Server struct {
|
||||
// because it includes intra-region forwarded packets as the
|
||||
// src.
|
||||
sentTo map[key.Public]map[key.Public]int64 // src => dst => dst's latest sclient.connNum
|
||||
|
||||
// maps from netaddr.IPPort to a client's public key
|
||||
keyOfAddr map[netaddr.IPPort]key.Public
|
||||
}
|
||||
|
||||
// PacketForwarder is something that can forward packets.
|
||||
@@ -161,7 +153,7 @@ type PacketForwarder interface {
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
// It is a defined type so that non-net connections can be used.
|
||||
type Conn interface {
|
||||
io.WriteCloser
|
||||
io.Closer
|
||||
|
||||
// The *Deadline methods follow the semantics of net.Conn.
|
||||
|
||||
@@ -183,29 +175,23 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
packetsDroppedType: metrics.LabelMap{Label: "type"},
|
||||
clients: map[key.Public]*sclient{},
|
||||
clientsEver: map[key.Public]bool{},
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
netConns: map[Conn]chan struct{}{},
|
||||
memSys0: ms.Sys,
|
||||
watchers: map[*sclient]bool{},
|
||||
sentTo: map[key.Public]map[key.Public]int64{},
|
||||
avgQueueDuration: new(uint64),
|
||||
keyOfAddr: map[netaddr.IPPort]key.Public{},
|
||||
}
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
|
||||
s.packetsDroppedReasonCounters = []*expvar.Int{
|
||||
s.packetsDroppedReason.Get("unknown_dest"),
|
||||
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
|
||||
s.packetsDroppedReason.Get("gone"),
|
||||
s.packetsDroppedReason.Get("queue_head"),
|
||||
s.packetsDroppedReason.Get("queue_tail"),
|
||||
s.packetsDroppedReason.Get("write_error"),
|
||||
}
|
||||
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
|
||||
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
|
||||
s.packetsDroppedUnknown = s.packetsDroppedReason.Get("unknown_dest")
|
||||
s.packetsDroppedFwdUnknown = s.packetsDroppedReason.Get("unknown_dest_on_fwd")
|
||||
s.packetsDroppedGone = s.packetsDroppedReason.Get("gone")
|
||||
s.packetsDroppedQueueHead = s.packetsDroppedReason.Get("queue_head")
|
||||
s.packetsDroppedQueueTail = s.packetsDroppedReason.Get("queue_tail")
|
||||
s.packetsDroppedWrite = s.packetsDroppedReason.Get("write_error")
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -217,13 +203,6 @@ func (s *Server) SetMeshKey(v string) {
|
||||
s.meshKey = v
|
||||
}
|
||||
|
||||
// SetVerifyClients sets whether this DERP server verifies clients through tailscaled.
|
||||
//
|
||||
// It must be called before serving begins.
|
||||
func (s *Server) SetVerifyClient(v bool) {
|
||||
s.verifyClients = v
|
||||
}
|
||||
|
||||
// HasMeshKey reports whether the server is configured with a mesh key.
|
||||
func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
|
||||
|
||||
@@ -343,40 +322,25 @@ func (s *Server) initMetacert() {
|
||||
func (s *Server) MetaCert() []byte { return s.metaCert }
|
||||
|
||||
// registerClient notes that client c is now authenticated and ready for packets.
|
||||
//
|
||||
// If c's public key was already connected with a different
|
||||
// connection, the prior one is closed, unless it's fighting rapidly
|
||||
// with another client with the same key, in which case the returned
|
||||
// ok is false, and the caller should wait the provided duration
|
||||
// before trying again.
|
||||
func (s *Server) registerClient(c *sclient) (ok bool, d time.Duration) {
|
||||
// If c's public key was already connected with a different connection, the prior one is closed.
|
||||
func (s *Server) registerClient(c *sclient) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.clients[c.key]
|
||||
if old == nil {
|
||||
c.logf("adding connection")
|
||||
} else {
|
||||
// Take over the old rate limiter, discarding the one
|
||||
// our caller just made.
|
||||
c.replaceLimiter = old.replaceLimiter
|
||||
if rr := c.replaceLimiter.ReserveN(timeNow(), 1); rr.OK() {
|
||||
if d := rr.DelayFrom(timeNow()); d > 0 {
|
||||
s.clientsReplaceLimited.Add(1)
|
||||
return false, d
|
||||
}
|
||||
}
|
||||
s.clientsReplaced.Add(1)
|
||||
c.logf("adding connection, replacing %s", old.remoteAddr)
|
||||
go old.nc.Close()
|
||||
}
|
||||
s.clients[c.key] = c
|
||||
s.clientsEver[c.key] = true
|
||||
if _, ok := s.clientsMesh[c.key]; !ok {
|
||||
s.clientsMesh[c.key] = nil // just for varz of total users in cluster
|
||||
}
|
||||
s.keyOfAddr[c.remoteIPPort] = c.key
|
||||
s.curClients.Add(1)
|
||||
s.broadcastPeerStateChangeLocked(c.key, true)
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// broadcastPeerStateChangeLocked enqueues a message to all watchers
|
||||
@@ -409,8 +373,6 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
delete(s.watchers, c)
|
||||
}
|
||||
|
||||
delete(s.keyOfAddr, c.remoteIPPort)
|
||||
|
||||
s.curClients.Add(-1)
|
||||
if c.preferred {
|
||||
s.curHomeClients.Add(-1)
|
||||
@@ -464,9 +426,8 @@ func (s *Server) addWatcher(c *sclient) {
|
||||
}
|
||||
|
||||
func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
|
||||
br := brw.Reader
|
||||
br, bw := brw.Reader, brw.Writer
|
||||
nc.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
bw := &lazyBufioWriter{w: nc, lbw: brw.Writer}
|
||||
if err := s.sendServerKey(bw); err != nil {
|
||||
return fmt.Errorf("send server key: %v", err)
|
||||
}
|
||||
@@ -485,32 +446,21 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
remoteIPPort, _ := netaddr.ParseIPPort(remoteAddr)
|
||||
|
||||
c := &sclient{
|
||||
connNum: connNum,
|
||||
s: s,
|
||||
key: clientKey,
|
||||
nc: nc,
|
||||
br: br,
|
||||
bw: bw,
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
|
||||
done: ctx.Done(),
|
||||
remoteAddr: remoteAddr,
|
||||
remoteIPPort: remoteIPPort,
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
peerGone: make(chan key.Public),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
|
||||
// Allow kicking out previous connections once a
|
||||
// minute, with a very high burst of 100. Once a
|
||||
// minute is less than the client's 2 minute
|
||||
// inactivity timeout.
|
||||
replaceLimiter: rate.NewLimiter(rate.Every(time.Minute), 100),
|
||||
connNum: connNum,
|
||||
s: s,
|
||||
key: clientKey,
|
||||
nc: nc,
|
||||
br: br,
|
||||
bw: bw,
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
|
||||
done: ctx.Done(),
|
||||
remoteAddr: remoteAddr,
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
peerGone: make(chan key.Public),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
c.meshUpdate = make(chan struct{})
|
||||
}
|
||||
@@ -518,18 +468,10 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
c.info = *clientInfo
|
||||
}
|
||||
|
||||
for {
|
||||
ok, d := s.registerClient(c)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
s.clientsReplaceSleeping.Add(1)
|
||||
timeSleep(d)
|
||||
s.clientsReplaceSleeping.Add(-1)
|
||||
}
|
||||
s.registerClient(c)
|
||||
defer s.unregisterClient(c)
|
||||
|
||||
err = s.sendServerInfo(c.bw, clientKey)
|
||||
err = s.sendServerInfo(bw, clientKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send server info: %v", err)
|
||||
}
|
||||
@@ -537,12 +479,6 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
return c.run(ctx)
|
||||
}
|
||||
|
||||
// for testing
|
||||
var (
|
||||
timeSleep = time.Sleep
|
||||
timeNow = time.Now
|
||||
)
|
||||
|
||||
// run serves the client until there's an error.
|
||||
// If the client hangs up or the server is closed, run returns nil, otherwise run returns an error.
|
||||
func (c *sclient) run(ctx context.Context) error {
|
||||
@@ -666,14 +602,17 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
s.mu.Unlock()
|
||||
|
||||
if dst == nil {
|
||||
s.recordDrop(contents, srcKey, dstKey, dropReasonUnknownDestOnFwd)
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedFwdUnknown.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping forwarded packet for unknown %x", dstKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.sendPkt(dst, pkt{
|
||||
bs: contents,
|
||||
enqueuedAt: time.Now(),
|
||||
src: srcKey,
|
||||
bs: contents,
|
||||
src: srcKey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -717,53 +656,21 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
s.recordDrop(contents, c.key, dstKey, dropReasonUnknownDest)
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedUnknown.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for unknown %x", dstKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
p := pkt{
|
||||
bs: contents,
|
||||
enqueuedAt: time.Now(),
|
||||
src: c.key,
|
||||
bs: contents,
|
||||
src: c.key,
|
||||
}
|
||||
return c.sendPkt(dst, p)
|
||||
}
|
||||
|
||||
// dropReason is why we dropped a DERP frame.
|
||||
type dropReason int
|
||||
|
||||
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file dropreason_string.go go run golang.org/x/tools/cmd/stringer -type=dropReason -trimprefix=dropReason
|
||||
|
||||
const (
|
||||
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
|
||||
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
|
||||
dropReasonGone // destination tailscaled disconnected before we could send
|
||||
dropReasonQueueHead // destination queue is full, dropped packet at queue head
|
||||
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
|
||||
dropReasonWriteError // OS write() failed
|
||||
)
|
||||
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.Public, reason dropReason) {
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedReasonCounters[reason].Add(1)
|
||||
if disco.LooksLikeDiscoWrapper(packetBytes) {
|
||||
s.packetsDroppedTypeDisco.Add(1)
|
||||
} else {
|
||||
s.packetsDroppedTypeOther.Add(1)
|
||||
}
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Preformat the log string prior to calling limitedLogf. The
|
||||
// limiter acts based on the format string, and we want to
|
||||
// rate-limit per src/dst keys, not on the generic "dropped
|
||||
// stuff" message.
|
||||
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
|
||||
s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
s := c.s
|
||||
dstKey := dst.key
|
||||
@@ -771,34 +678,53 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// Attempt to queue for sending up to 3 times. On each attempt, if
|
||||
// the queue is full, try to drop from queue head to prioritize
|
||||
// fresher packets.
|
||||
sendQueue := dst.sendQueue
|
||||
if disco.LooksLikeDiscoWrapper(p.bs) {
|
||||
sendQueue = dst.discoSendQueue
|
||||
}
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
select {
|
||||
case <-dst.done:
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedGone.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for shutdown client %x", dstKey)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case sendQueue <- p:
|
||||
case dst.sendQueue <- p:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case pkt := <-sendQueue:
|
||||
s.recordDrop(pkt.bs, c.key, dstKey, dropReasonQueueHead)
|
||||
c.recordQueueTime(pkt.enqueuedAt)
|
||||
case <-dst.sendQueue:
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueHead.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("tail drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue head", dstKey)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
// Failed to make room for packet. This can happen in a heavily
|
||||
// contended queue with racing writers. Give up and tail-drop in
|
||||
// this case to keep reader unblocked.
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueTail.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("head drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue tail", dstKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -824,37 +750,23 @@ func (c *sclient) requestMeshUpdate() {
|
||||
}
|
||||
|
||||
func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
if !s.verifyClients {
|
||||
return nil
|
||||
}
|
||||
status, err := tailscale.Status(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query local tailscaled status: %w", err)
|
||||
}
|
||||
if clientKey == status.Self.PublicKey {
|
||||
return nil
|
||||
}
|
||||
if _, exists := status.Peer[clientKey]; !exists {
|
||||
return fmt.Errorf("client %v not in set of peers", clientKey)
|
||||
}
|
||||
// TODO(bradfitz): add policy for configurable bandwidth rate per client?
|
||||
// TODO(crawshaw): implement policy constraints on who can use the DERP server
|
||||
// TODO(bradfitz): ... and at what rate.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
|
||||
func (s *Server) sendServerKey(bw *bufio.Writer) error {
|
||||
buf := make([]byte, 0, len(magic)+len(s.publicKey))
|
||||
buf = append(buf, magic...)
|
||||
buf = append(buf, s.publicKey[:]...)
|
||||
err := writeFrame(lw.bw(), frameServerKey, buf)
|
||||
lw.Flush() // redundant (no-op) flush to release bufio.Writer
|
||||
return err
|
||||
return writeFrame(bw, frameServerKey, buf)
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error {
|
||||
func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
var nonce [24]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
@@ -865,7 +777,7 @@ func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error
|
||||
}
|
||||
|
||||
msgbox := box.Seal(nil, msg, &nonce, clientKey.B32(), s.privateKey.B32())
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
if err := writeFrameHeader(bw, frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(nonce[:]); err != nil {
|
||||
@@ -973,25 +885,18 @@ func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, d
|
||||
// (The "s" prefix is to more explicitly distinguish it from Client in derp_client.go)
|
||||
type sclient struct {
|
||||
// Static after construction.
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
|
||||
// replaceLimiter controls how quickly two connections with
|
||||
// the same client key can kick each other off the server by
|
||||
// taking over ownership of a key.
|
||||
replaceLimiter *rate.Limiter
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
|
||||
// Owned by run, not thread-safe.
|
||||
br *bufio.Reader
|
||||
@@ -999,7 +904,7 @@ type sclient struct {
|
||||
preferred bool
|
||||
|
||||
// Owned by sender, not thread-safe.
|
||||
bw *lazyBufioWriter
|
||||
bw *bufio.Writer
|
||||
|
||||
// Guarded by s.mu
|
||||
//
|
||||
@@ -1022,13 +927,11 @@ type pkt struct {
|
||||
// src is the who's the sender of the packet.
|
||||
src key.Public
|
||||
|
||||
// enqueuedAt is when a packet was put onto a queue before it was sent,
|
||||
// and is used for reporting metrics on the duration of packets in the queue.
|
||||
enqueuedAt time.Time
|
||||
|
||||
// bs is the data packet bytes.
|
||||
// The memory is owned by pkt.
|
||||
bs []byte
|
||||
|
||||
// TODO(danderson): enqueue time, to measure queue latency?
|
||||
}
|
||||
|
||||
func (c *sclient) setPreferred(v bool) {
|
||||
@@ -1056,25 +959,6 @@ func (c *sclient) setPreferred(v bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// expMovingAverage returns the new moving average given the previous average,
|
||||
// a new value, and an alpha decay factor.
|
||||
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
||||
func expMovingAverage(prev, newValue, alpha float64) float64 {
|
||||
return alpha*newValue + (1-alpha)*prev
|
||||
}
|
||||
|
||||
// recordQueueTime updates the average queue duration metric after a packet has been sent.
|
||||
func (c *sclient) recordQueueTime(enqueuedAt time.Time) {
|
||||
elapsed := float64(time.Since(enqueuedAt).Milliseconds())
|
||||
for {
|
||||
old := atomic.LoadUint64(c.s.avgQueueDuration)
|
||||
newAvg := expMovingAverage(math.Float64frombits(old), elapsed, 0.1)
|
||||
if atomic.CompareAndSwapUint64(c.s.avgQueueDuration, old, math.Float64bits(newAvg)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
defer func() {
|
||||
// If the sender shuts down unilaterally due to an error, close so
|
||||
@@ -1084,10 +968,12 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
// Drain the send queue to count dropped packets
|
||||
for {
|
||||
select {
|
||||
case pkt := <-c.sendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
case pkt := <-c.discoSendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
case <-c.sendQueue:
|
||||
c.s.packetsDropped.Add(1)
|
||||
c.s.packetsDroppedGone.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for shutdown %x", c.key)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -1116,11 +1002,6 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
continue
|
||||
case msg := <-c.sendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
@@ -1144,10 +1025,6 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
continue
|
||||
case msg := <-c.sendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
}
|
||||
@@ -1161,14 +1038,14 @@ func (c *sclient) setWriteDeadline() {
|
||||
// sendKeepAlive sends a keep-alive frame, without flushing.
|
||||
func (c *sclient) sendKeepAlive() error {
|
||||
c.setWriteDeadline()
|
||||
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
|
||||
return writeFrameHeader(c.bw, frameKeepAlive, 0)
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw, framePeerGone, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
@@ -1178,7 +1055,7 @@ func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.Public) error {
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw, framePeerPresent, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
@@ -1237,7 +1114,11 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
defer func() {
|
||||
// Stats update.
|
||||
if err != nil {
|
||||
c.s.recordDrop(contents, srcKey, c.key, dropReasonWriteError)
|
||||
c.s.packetsDropped.Add(1)
|
||||
c.s.packetsDroppedWrite.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet to %x: %v", c.key, err)
|
||||
}
|
||||
} else {
|
||||
c.s.packetsSent.Add(1)
|
||||
c.s.bytesSent.Add(int64(len(contents)))
|
||||
@@ -1251,11 +1132,11 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
if withKey {
|
||||
pktLen += len(srcKey)
|
||||
}
|
||||
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
if err = writeFrameHeader(c.bw, frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
err := writePublicKey(c.bw.bw(), &srcKey)
|
||||
err := writePublicKey(c.bw, &srcKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1383,6 +1264,7 @@ func (s *Server) expVarFunc(f func() interface{}) expvar.Func {
|
||||
// ExpVar returns an expvar variable suitable for registering with expvar.Publish.
|
||||
func (s *Server) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_unique_clients_ever", s.expVarFunc(func() interface{} { return len(s.clientsEver) }))
|
||||
m.Set("gauge_memstats_sys0", expvar.Func(func() interface{} { return int64(s.memSys0) }))
|
||||
m.Set("gauge_watchers", s.expVarFunc(func() interface{} { return len(s.watchers) }))
|
||||
m.Set("gauge_current_connections", &s.curClients)
|
||||
@@ -1392,13 +1274,10 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("gauge_clients_remote", expvar.Func(func() interface{} { return len(s.clientsMesh) - len(s.clients) }))
|
||||
m.Set("accepts", &s.accepts)
|
||||
m.Set("clients_replaced", &s.clientsReplaced)
|
||||
m.Set("clients_replace_limited", &s.clientsReplaceLimited)
|
||||
m.Set("gauge_clients_replace_sleeping", &s.clientsReplaceSleeping)
|
||||
m.Set("bytes_received", &s.bytesRecv)
|
||||
m.Set("bytes_sent", &s.bytesSent)
|
||||
m.Set("packets_dropped", &s.packetsDropped)
|
||||
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
|
||||
m.Set("counter_packets_dropped_type", &s.packetsDroppedType)
|
||||
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
|
||||
m.Set("packets_sent", &s.packetsSent)
|
||||
m.Set("packets_received", &s.packetsRecv)
|
||||
@@ -1411,9 +1290,6 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("multiforwarder_created", &s.multiForwarderCreated)
|
||||
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
|
||||
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
|
||||
m.Set("average_queue_duration_ms", expvar.Func(func() interface{} {
|
||||
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
|
||||
}))
|
||||
var expvarVersion expvar.String
|
||||
expvarVersion.Set(version.Long)
|
||||
m.Set("version", &expvarVersion)
|
||||
@@ -1489,126 +1365,3 @@ func writePublicKey(bw *bufio.Writer, key *key.Public) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const minTimeBetweenLogs = 2 * time.Second
|
||||
|
||||
// BytesSentRecv records the number of bytes that have been sent since the last traffic check
|
||||
// for a given process, as well as the public key of the process sending those bytes.
|
||||
type BytesSentRecv struct {
|
||||
Sent uint64
|
||||
Recv uint64
|
||||
// Key is the public key of the client which sent/received these bytes.
|
||||
Key key.Public
|
||||
}
|
||||
|
||||
// parseSSOutput parses the output from the specific call to ss in ServeDebugTraffic.
|
||||
// Separated out for ease of testing.
|
||||
func parseSSOutput(raw string) map[netaddr.IPPort]BytesSentRecv {
|
||||
newState := map[netaddr.IPPort]BytesSentRecv{}
|
||||
// parse every 2 lines and get src and dst ips, and kv pairs
|
||||
lines := strings.Split(raw, "\n")
|
||||
for i := 0; i < len(lines); i += 2 {
|
||||
ipInfo := strings.Fields(strings.TrimSpace(lines[i]))
|
||||
if len(ipInfo) < 5 {
|
||||
continue
|
||||
}
|
||||
src, err := netaddr.ParseIPPort(ipInfo[4])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
stats := strings.Fields(strings.TrimSpace(lines[i+1]))
|
||||
stat := BytesSentRecv{}
|
||||
for _, s := range stats {
|
||||
if strings.Contains(s, "bytes_sent") {
|
||||
sent, err := strconv.Atoi(s[strings.Index(s, ":")+1:])
|
||||
if err == nil {
|
||||
stat.Sent = uint64(sent)
|
||||
}
|
||||
} else if strings.Contains(s, "bytes_received") {
|
||||
recv, err := strconv.Atoi(s[strings.Index(s, ":")+1:])
|
||||
if err == nil {
|
||||
stat.Recv = uint64(recv)
|
||||
}
|
||||
}
|
||||
}
|
||||
newState[src] = stat
|
||||
}
|
||||
return newState
|
||||
}
|
||||
|
||||
func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
prevState := map[netaddr.IPPort]BytesSentRecv{}
|
||||
enc := json.NewEncoder(w)
|
||||
for r.Context().Err() == nil {
|
||||
output, err := exec.Command("ss", "-i", "-H", "-t").Output()
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "ss failed: %v", err)
|
||||
return
|
||||
}
|
||||
newState := parseSSOutput(string(output))
|
||||
s.mu.Lock()
|
||||
for k, next := range newState {
|
||||
prev := prevState[k]
|
||||
if prev.Sent < next.Sent || prev.Recv < next.Recv {
|
||||
if pkey, ok := s.keyOfAddr[k]; ok {
|
||||
next.Key = pkey
|
||||
if err := enc.Encode(next); err != nil {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
prevState = newState
|
||||
if _, err := fmt.Fprintln(w); err != nil {
|
||||
return
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
time.Sleep(minTimeBetweenLogs)
|
||||
}
|
||||
}
|
||||
|
||||
var bufioWriterPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
|
||||
},
|
||||
}
|
||||
|
||||
// lazyBufioWriter is a bufio.Writer-like wrapping writer that lazily
|
||||
// allocates its actual bufio.Writer from a sync.Pool, releasing it to
|
||||
// the pool upon flush.
|
||||
//
|
||||
// We do this to reduce memory overhead; most DERP connections are
|
||||
// idle and the idle bufio.Writers were 30% of overall memory usage.
|
||||
type lazyBufioWriter struct {
|
||||
w io.Writer // underlying
|
||||
lbw *bufio.Writer // lazy; nil means it needs an associated buffer
|
||||
}
|
||||
|
||||
func (w *lazyBufioWriter) bw() *bufio.Writer {
|
||||
if w.lbw == nil {
|
||||
w.lbw = bufioWriterPool.Get().(*bufio.Writer)
|
||||
w.lbw.Reset(w.w)
|
||||
}
|
||||
return w.lbw
|
||||
}
|
||||
|
||||
func (w *lazyBufioWriter) Available() int { return w.bw().Available() }
|
||||
|
||||
func (w *lazyBufioWriter) Write(p []byte) (int, error) { return w.bw().Write(p) }
|
||||
|
||||
func (w *lazyBufioWriter) Flush() error {
|
||||
if w.lbw == nil {
|
||||
return nil
|
||||
}
|
||||
err := w.lbw.Flush()
|
||||
|
||||
w.lbw.Reset(ioutil.Discard)
|
||||
bufioWriterPool.Put(w.lbw)
|
||||
w.lbw = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
@@ -23,7 +22,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -410,7 +408,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
for i := 0; i < cap(errCh); i++ {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
if errors.Is(err, io.EOF) {
|
||||
continue
|
||||
}
|
||||
t.Error(err)
|
||||
@@ -793,193 +791,6 @@ func TestMetaCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type dummyNetConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (dummyNetConn) SetReadDeadline(time.Time) error { return nil }
|
||||
|
||||
func TestClientRecv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
name: "ping",
|
||||
input: []byte{
|
||||
byte(framePing), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
nc: dummyNetConn{},
|
||||
br: bufio.NewReader(bytes.NewReader(tt.input)),
|
||||
logf: t.Logf,
|
||||
}
|
||||
got, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %#v; want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPong(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(&buf),
|
||||
}
|
||||
if err := c.SendPong([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{
|
||||
byte(framePong), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServerReplaceClients(t *testing.T) {
|
||||
defer func() {
|
||||
timeSleep = time.Sleep
|
||||
timeNow = time.Now
|
||||
}()
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
now = time.Unix(123, 0)
|
||||
sleeps int
|
||||
slept time.Duration
|
||||
)
|
||||
timeSleep = func(d time.Duration) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
sleeps++
|
||||
slept += d
|
||||
now = now.Add(d)
|
||||
}
|
||||
timeNow = func() time.Time {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return now
|
||||
}
|
||||
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
var logger logger.Logf = logger.Discard
|
||||
const debug = false
|
||||
if debug {
|
||||
logger = t.Logf
|
||||
}
|
||||
|
||||
s := NewServer(serverPrivateKey, logger)
|
||||
defer s.Close()
|
||||
|
||||
priv := newPrivateKey(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
connNum := 0
|
||||
connect := func() *Client {
|
||||
connNum++
|
||||
cout, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cin, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
|
||||
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", connNum))
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
|
||||
c, err := NewClient(priv, cout, brw, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", connNum, err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
wantVar := func(v *expvar.Int, want int64) {
|
||||
t.Helper()
|
||||
if got := v.Value(); got != want {
|
||||
t.Errorf("got %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
wantClosed := func(c *Client) {
|
||||
t.Helper()
|
||||
for {
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
}
|
||||
switch m.(type) {
|
||||
case ServerInfoMessage:
|
||||
continue
|
||||
default:
|
||||
t.Fatalf("client got %T; wanted an error", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c1 := connect()
|
||||
waitConnect(t, c1)
|
||||
c2 := connect()
|
||||
waitConnect(t, c2)
|
||||
wantVar(&s.clientsReplaced, 1)
|
||||
wantClosed(c1)
|
||||
|
||||
for i := 0; i < 100+5; i++ {
|
||||
c := connect()
|
||||
defer c.nc.Close()
|
||||
if s.clientsReplaceLimited.Value() == 0 && i < 90 {
|
||||
continue
|
||||
}
|
||||
t.Logf("for %d: replaced=%d, limited=%d, sleeping=%d", i,
|
||||
s.clientsReplaced.Value(),
|
||||
s.clientsReplaceLimited.Value(),
|
||||
s.clientsReplaceSleeping.Value(),
|
||||
)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if sleeps == 0 {
|
||||
t.Errorf("no sleeps")
|
||||
}
|
||||
if slept == 0 {
|
||||
t.Errorf("total sleep duration was 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiter(t *testing.T) {
|
||||
rl := rate.NewLimiter(rate.Every(time.Minute), 100)
|
||||
for i := 0; i < 200; i++ {
|
||||
r := rl.Reserve()
|
||||
d := r.Delay()
|
||||
t.Logf("i=%d, allow=%v, d=%v", i, r.OK(), d)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000, 10000} {
|
||||
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
|
||||
@@ -1079,14 +890,3 @@ func waitConnect(t testing.TB, c *Client) {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSSOutput(t *testing.T) {
|
||||
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
|
||||
if err != nil {
|
||||
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
|
||||
}
|
||||
seen := parseSSOutput(string(contents))
|
||||
if len(seen) == 0 {
|
||||
t.Errorf("parseSSOutput expected non-empty map")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,11 +50,9 @@ type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
privateKey key.Private
|
||||
logf logger.Logf
|
||||
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Either url or getRegion is non-nil:
|
||||
url *url.URL
|
||||
@@ -65,7 +63,6 @@ type Client struct {
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
@@ -132,11 +129,6 @@ func (c *Client) ServerPublicKey() key.Public {
|
||||
return c.serverPubKey
|
||||
}
|
||||
|
||||
// SelfPublicKey returns our own public key.
|
||||
func (c *Client) SelfPublicKey() key.Public {
|
||||
return c.privateKey.Public()
|
||||
}
|
||||
|
||||
func urlPort(u *url.URL) string {
|
||||
if p := u.Port(); p != "" {
|
||||
return p
|
||||
@@ -341,12 +333,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
}
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.ServerPublicKey(serverPub),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
derp.IsProber(c.IsProber),
|
||||
)
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -364,26 +351,14 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
// SetURLDialer sets the dialer to use for dialing URLs.
|
||||
// This dialer is only use for clients created with NewClient, not NewRegionClient.
|
||||
// If unset or nil, the default dialer is used.
|
||||
//
|
||||
// The primary use for this is the derper mesh mode to connect to each
|
||||
// other over a VPC network.
|
||||
func (c *Client) SetURLDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||
c.dialer = dialer
|
||||
}
|
||||
|
||||
func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
host := c.url.Hostname()
|
||||
if c.dialer != nil {
|
||||
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
|
||||
}
|
||||
hostOrIP := host
|
||||
|
||||
dialer := netns.NewDialer()
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
ip, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err == nil {
|
||||
hostOrIP = ip.String()
|
||||
}
|
||||
@@ -430,7 +405,9 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
|
||||
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig)
|
||||
if node != nil {
|
||||
tlsConf.InsecureSkipVerify = node.InsecureForTests
|
||||
if node.DERPTestPort != 0 {
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
}
|
||||
if node.CertName != "" {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
@@ -529,8 +506,8 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
dst = n.HostName
|
||||
}
|
||||
port := "443"
|
||||
if n.DERPPort != 0 {
|
||||
port = fmt.Sprint(n.DERPPort)
|
||||
if n.DERPTestPort != 0 {
|
||||
port = fmt.Sprint(n.DERPTestPort)
|
||||
}
|
||||
c, err := c.dialContext(ctx, proto, net.JoinHostPort(dst, port))
|
||||
select {
|
||||
@@ -665,38 +642,6 @@ func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SendPong sends a reply to a ping, with the ping's provided
|
||||
// challenge/identifier data.
|
||||
//
|
||||
// Unlike other send methods, SendPong makes no attempt to connect or
|
||||
// reconnect to the peer. It's best effort. If there's a connection
|
||||
// problem, the server will choose to hang up on us if we're not
|
||||
// replying.
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return ErrClientClosed
|
||||
}
|
||||
if c.client == nil {
|
||||
c.mu.Unlock()
|
||||
return errors.New("not connected")
|
||||
}
|
||||
dc := c.client
|
||||
c.mu.Unlock()
|
||||
|
||||
return dc.SendPong(data)
|
||||
}
|
||||
|
||||
// SetCanAckPings sets whether this client will reply to ping requests from the server.
|
||||
//
|
||||
// This only affects future connections.
|
||||
func (c *Client) SetCanAckPings(v bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.canAckPings = v
|
||||
}
|
||||
|
||||
// NotePreferred notes whether this Client is the caller's preferred
|
||||
// (home) DERP node. It's only used for stats.
|
||||
func (c *Client) NotePreferred(v bool) {
|
||||
@@ -764,19 +709,10 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
}
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
|
||||
@@ -5,32 +5,20 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to
|
||||
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
|
||||
// connection changes.
|
||||
//
|
||||
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
|
||||
//
|
||||
// Otherwise, the add and remove funcs are called as clients come & go.
|
||||
//
|
||||
// infoLogf, if non-nil, is the logger to write periodic status
|
||||
// updates about how many peers are on the server. Error log output is
|
||||
// set to the c's logger, regardless of infoLogf's value.
|
||||
//
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
|
||||
// be closed, and c itself needs to be closed.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
|
||||
if infoLogf == nil {
|
||||
infoLogf = logger.Discard
|
||||
}
|
||||
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
|
||||
logf := c.logf
|
||||
const retryInterval = 5 * time.Second
|
||||
const statusInterval = 10 * time.Second
|
||||
@@ -57,7 +45,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
if loggedConnected {
|
||||
return
|
||||
}
|
||||
infoLogf("connected; %d peers", len(present))
|
||||
logf("connected; %d peers", len(present))
|
||||
loggedConnected = true
|
||||
}
|
||||
|
||||
@@ -91,21 +79,12 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
}
|
||||
|
||||
sleep := func(d time.Duration) {
|
||||
t := time.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
for {
|
||||
err := c.WatchConnectionChanges()
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("WatchConnectionChanges: %v", err)
|
||||
sleep(retryInterval)
|
||||
time.Sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -118,7 +97,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("Recv: %v", err)
|
||||
sleep(retryInterval)
|
||||
time.Sleep(retryInterval)
|
||||
break
|
||||
}
|
||||
if connGen != lastConnGen {
|
||||
@@ -135,8 +114,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
|
||||
lastStatus = now
|
||||
infoLogf("%d peers", len(present))
|
||||
logf("%d peers", len(present))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
88
derp/derpmap/derpmap.go
Normal file
88
derp/derpmap/derpmap.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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 derpmap contains information about Tailscale.com's production DERP nodes.
|
||||
//
|
||||
// This package is only used by the "tailscale netcheck" command for debugging.
|
||||
// In normal operation the Tailscale nodes get this sent to them from the control
|
||||
// server.
|
||||
//
|
||||
// TODO: remove this package and make "tailscale netcheck" get the
|
||||
// list from the control server too.
|
||||
package derpmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
|
||||
return &tailcfg.DERPNode{
|
||||
Name: suffix, // updated later
|
||||
RegionID: 0, // updated later
|
||||
IPv4: v4,
|
||||
IPv6: v6,
|
||||
}
|
||||
}
|
||||
|
||||
func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
region := &tailcfg.DERPRegion{
|
||||
RegionID: id,
|
||||
RegionName: name,
|
||||
RegionCode: code,
|
||||
Nodes: nodes,
|
||||
}
|
||||
for _, n := range nodes {
|
||||
n.Name = fmt.Sprintf("%d%s", id, n.Name)
|
||||
n.RegionID = id
|
||||
n.HostName = fmt.Sprintf("derp%s.tailscale.com", strings.TrimSuffix(n.Name, "a"))
|
||||
}
|
||||
return region
|
||||
}
|
||||
|
||||
// Prod returns Tailscale's map of relay servers.
|
||||
//
|
||||
// This list is only used by cmd/tailscale's netcheck subcommand. In
|
||||
// normal operation the Tailscale nodes get this sent to them from the
|
||||
// control server.
|
||||
//
|
||||
// This list is subject to change and should not be relied on.
|
||||
func Prod() *tailcfg.DERPMap {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: derpRegion(1, "nyc", "New York City",
|
||||
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
|
||||
),
|
||||
2: derpRegion(2, "sfo", "San Francisco",
|
||||
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
|
||||
),
|
||||
3: derpRegion(3, "sin", "Singapore",
|
||||
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
|
||||
),
|
||||
4: derpRegion(4, "fra", "Frankfurt",
|
||||
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
|
||||
),
|
||||
5: derpRegion(5, "syd", "Sydney",
|
||||
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
|
||||
),
|
||||
6: derpRegion(6, "blr", "Bangalore",
|
||||
derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"),
|
||||
),
|
||||
7: derpRegion(7, "tok", "Tokyo",
|
||||
derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"),
|
||||
),
|
||||
8: derpRegion(8, "lhr", "London",
|
||||
derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"),
|
||||
),
|
||||
9: derpRegion(9, "dfw", "Dallas",
|
||||
derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"),
|
||||
),
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user