Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a9656c01 | ||
|
|
b26876427c | ||
|
|
cc2ec141fe | ||
|
|
d2c1ae7ed4 | ||
|
|
121f5a00f7 | ||
|
|
d06ceffd02 | ||
|
|
0cf60b5185 | ||
|
|
910682c851 | ||
|
|
c027962893 | ||
|
|
acc50d6b67 | ||
|
|
a2ab23ba6c | ||
|
|
71b13b5ac2 | ||
|
|
1c238cdce6 | ||
|
|
a9f58fe822 | ||
|
|
5417ca69a7 | ||
|
|
03e640e94d | ||
|
|
138bcae525 | ||
|
|
bb0ef32dd2 | ||
|
|
dde7ba4ecf | ||
|
|
fc30cff688 | ||
|
|
775fe13e27 | ||
|
|
2e33fdfe67 | ||
|
|
3d7cff91b3 |
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.
|
||||
|
||||
48
.github/workflows/coverage.yml
vendored
Normal file
48
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Code Coverage
|
||||
|
||||
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.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# https://markphelps.me/2019/11/speed-up-your-go-builds-with-actions-cache/
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@preview
|
||||
id: cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux with coverage data
|
||||
run: go test -race -coverprofile=coverage.txt -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: coveralls.io
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.COVERALLS_BOT_PUBLIC_REPO_TOKEN }}
|
||||
with:
|
||||
path-to-profile: ./coverage.txt
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -38,7 +38,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.16-alpine AS build-env
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -48,19 +48,10 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# see build_docker.sh
|
||||
ARG VERSION_LONG=""
|
||||
ENV VERSION_LONG=$VERSION_LONG
|
||||
ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
ARG goflags_arg # default intentionally unset
|
||||
ENV GOFLAGS=$goflags_arg
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.9.0
|
||||
1.4.4
|
||||
|
||||
39
api.md
39
api.md
@@ -367,11 +367,10 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
|
||||
#### `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.
|
||||
Sets the ACL for the given tailnet. 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.
|
||||
Returns error for invalid ACLs.
|
||||
Returns error if using an `If-Match` header and the ETag does not match.
|
||||
|
||||
##### Parameters
|
||||
|
||||
@@ -381,17 +380,7 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
`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.
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
@@ -422,7 +411,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
Response
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
@@ -447,25 +436,9 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
@@ -504,7 +477,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@exampl
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
Response
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
@@ -27,8 +27,5 @@ set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
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 .
|
||||
GOFLAGS='-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}"
|
||||
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -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,258 +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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -48,7 +48,6 @@ 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")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -146,8 +145,6 @@ func main() {
|
||||
// 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)
|
||||
@@ -156,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>
|
||||
`)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -41,6 +40,6 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
c.MeshKey = s.MeshKey()
|
||||
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,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>
|
||||
145
cmd/tailscale/cli/cli.go
Normal file
145
cmd/tailscale/cli/cli.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 contains the cmd/tailscale CLI code in a package that can be included
|
||||
// in other wrapper binaries such as the Mac and Windows clients.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
if len(os.Args) < 2 {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) error {
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
return c, bc, ctx, cancel
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
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
|
||||
}
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -27,26 +28,28 @@ import (
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
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.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
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -60,24 +63,24 @@ func debugMode(args []string) error {
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
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)
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
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())
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
66
cmd/tailscale/cli/down.go
Normal file
66
cmd/tailscale/cli/down.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, func() {
|
||||
log.Fatalf("timeout running stop")
|
||||
})
|
||||
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
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -44,10 +43,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: ")),
|
||||
}
|
||||
c := &netcheck.Client{}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
c.Verbose = true
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -72,6 +69,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
var ip string
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
stc := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
@@ -79,16 +77,46 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
}
|
||||
if n.Status != nil {
|
||||
stc <- n.Status
|
||||
}
|
||||
})
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(ctx, bc, c) }()
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-stc:
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
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)
|
||||
}
|
||||
@@ -97,13 +125,11 @@ func runPing(ctx context.Context, args []string) error {
|
||||
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:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
@@ -114,20 +140,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 +153,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,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -18,8 +19,6 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -28,7 +27,7 @@ import (
|
||||
|
||||
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 {
|
||||
@@ -38,7 +37,7 @@ var statusCmd = &ffcli.Command{
|
||||
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
|
||||
})(),
|
||||
@@ -55,7 +54,42 @@ var statusArgs struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
select {
|
||||
case ch <- n.Status:
|
||||
default:
|
||||
// A status update from somebody else's request.
|
||||
// Ignoring this matters mostly for "tailscale status -web"
|
||||
// mode, otherwise the channel send would block forever
|
||||
// and pump would stop reading from tailscaled, which
|
||||
// previously caused tailscaled to block (while holding
|
||||
// a mutex), backing up unrelated clients.
|
||||
// See https://github.com/tailscale/tailscale/issues/1234
|
||||
}
|
||||
}
|
||||
})
|
||||
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 err
|
||||
}
|
||||
@@ -93,7 +127,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
|
||||
@@ -107,24 +141,9 @@ 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
|
||||
@@ -132,7 +151,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
firstIPString(ps.TailscaleIPs),
|
||||
ps.TailAddr,
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
ownerLogin(st, ps),
|
||||
ps.OS,
|
||||
@@ -140,18 +159,13 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
if anyTraffic {
|
||||
f("idle")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
} else {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
@@ -197,11 +211,13 @@ func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
return baseName
|
||||
if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) {
|
||||
return ps.DNSName[:i]
|
||||
}
|
||||
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
||||
if ps.DNSName != "" {
|
||||
return strings.TrimRight(ps.DNSName, ".")
|
||||
}
|
||||
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
@@ -217,10 +233,3 @@ func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
}
|
||||
return u.LoginName
|
||||
}
|
||||
|
||||
func firstIPString(v []netaddr.IP) string {
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
return v[0].String()
|
||||
}
|
||||
295
cmd/tailscale/cli/up.go
Normal file
295
cmd/tailscale/cli/up.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
Name: "up",
|
||||
ShortUsage: "up [flags]",
|
||||
ShortHelp: "Connect to your Tailscale network",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
"tailscale up" connects this machine to your Tailscale network,
|
||||
triggering authentication if necessary.
|
||||
|
||||
The flags passed to this command are specific to this machine. If you don't
|
||||
specify any flags, options are reset to their default.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
}
|
||||
return upf
|
||||
})(),
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
return "off"
|
||||
}
|
||||
return "on"
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings if IP forwarding is not
|
||||
// enabled, or if we were unable to verify the state of IP forwarding.
|
||||
func checkIPForwarding() {
|
||||
var key string
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
key = "net.ipv4.ip_forward"
|
||||
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
warnf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
warnf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
if !on {
|
||||
warnf("%s is disabled. Subnet routes won't work.", key)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
return errors.New("--advertise-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
var printed bool
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
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 [...]')"
|
||||
}
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
// StateKey and send the prefs directly. Although the Windows
|
||||
// supports server mode, though, the transition to StateStore
|
||||
// is only half complete. Only server mode uses it, and the
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection idenity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
// on our connection's identity.
|
||||
opts.StateKey = ""
|
||||
opts.Prefs = prefs
|
||||
}
|
||||
|
||||
// We still have to Start right now because it's the only way to
|
||||
// set up notifications and whatnot. This causes a bunch of churn
|
||||
// every time the CLI touches anything.
|
||||
//
|
||||
// TODO(danderson): redo the frontend/backend API to assume
|
||||
// ephemeral frontends that read/modify/write state, once
|
||||
// Windows/Mac state is moved into backend.
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
}
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,94 +2,135 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote 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
|
||||
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+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
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/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+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
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/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
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/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
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/ipn 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/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/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/packet from tailscale.com/wgengine+
|
||||
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+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
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/tstime from tailscale.com/wgengine/magicsock
|
||||
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/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
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/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
L tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd 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/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
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/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
|
||||
tailscale.com/wgengine/wglog 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+
|
||||
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 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
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+
|
||||
@@ -117,8 +158,7 @@ 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 from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
@@ -132,36 +172,36 @@ 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/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
html/template from tailscale.com/cmd/tailscale/cli
|
||||
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
|
||||
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+
|
||||
@@ -169,8 +209,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+
|
||||
|
||||
@@ -4,23 +4,24 @@
|
||||
|
||||
// The tailscale command is the Tailscale command-line client. It interacts
|
||||
// with the tailscaled node agent.
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/cmd/tailscaled/cli"
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func tailscale_main() {
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
func main() {
|
||||
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,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
|
||||
}
|
||||
@@ -1,276 +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 contains the cmd/tailscale CLI code in a package that can be included
|
||||
// in other wrapper binaries such as the Mac and Windows clients.
|
||||
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" {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) error {
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
For help on subcommands, add --help after: "tailscale status --help".
|
||||
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
logoutCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
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)
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
return c, bc, ctx, cancel
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return fmt.Errorf("%w (tailscaled stopped running?)", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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,668 +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"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"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",
|
||||
},
|
||||
}
|
||||
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(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
|
||||
goos: goos,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
|
||||
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
|
||||
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
localCreds bool
|
||||
goroutines bool
|
||||
ipn bool
|
||||
netMap bool
|
||||
file string
|
||||
prefs bool
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.localCreds {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
if debugArgs.prefs {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugArgs.pretty {
|
||||
fmt.Println(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
fmt.Println(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.ipn {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !debugArgs.netMap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
if debugArgs.file != "" {
|
||||
if debugArgs.file == "get" {
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
}
|
||||
delete := strings.HasPrefix(debugArgs.file, "delete:")
|
||||
if delete {
|
||||
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
|
||||
}
|
||||
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Size: %v\n", size)
|
||||
io.Copy(os.Stdout, rc)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
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,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -1,444 +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 {
|
||||
//lint:ignore ST1005 no sorry need that colon at the end
|
||||
return errors.New("usage: tailscale file cp <files...> <target>:")
|
||||
}
|
||||
files, target := args[:len(args)-1], args[len(args)-1]
|
||||
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, lastSeen, 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)
|
||||
} else if !lastSeen.IsZero() && time.Since(lastSeen) > lastSeenOld {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s last seen %v ago\n", target, time.Since(lastSeen).Round(time.Minute))
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
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, lastSeen time.Time, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.IP() != ip {
|
||||
continue
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
lastSeen = *n.LastSeen
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, lastSeen, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", time.Time{}, 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
|
||||
}
|
||||
|
||||
const lastSeenOld = 20 * time.Minute
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,775 +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"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
Name: "up",
|
||||
ShortUsage: "up [flags]",
|
||||
ShortHelp: "Connect to Tailscale, logging in if needed",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
"tailscale up" connects this machine to your Tailscale network,
|
||||
triggering authentication if necessary.
|
||||
|
||||
With no flags, "tailscale up" brings the network online without
|
||||
changing any settings. (That is, it's the opposite of "tailscale
|
||||
down").
|
||||
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset
|
||||
flag is also used.
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(runtime.GOOS, &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
switch goos {
|
||||
case "linux":
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
case "windows":
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
return upf
|
||||
}
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
return "off"
|
||||
}
|
||||
return "on"
|
||||
}
|
||||
|
||||
type upArgsT struct {
|
||||
reset bool
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
forceDaemon bool
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
opUser string
|
||||
}
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
|
||||
for r := range routeMap {
|
||||
routes = append(routes, r)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Bits() != routes[j].Bits() {
|
||||
return routes[i].Bits() < routes[j].Bits()
|
||||
}
|
||||
return routes[i].IP().Less(routes[j].IP())
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
}
|
||||
|
||||
if upArgs.exitNodeIP != "" {
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if exitNodeIP == ip {
|
||||
return nil, fmt.Errorf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", upArgs.exitNodeIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = upArgs.forceDaemon
|
||||
prefs.OperatorUser = upArgs.opUser
|
||||
|
||||
if goos == "linux" {
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
fatalf("can't fetch status from tailscaled: %v", err)
|
||||
}
|
||||
origAuthURL := st.AuthURL
|
||||
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
if upArgs.authKey != "" {
|
||||
// Issue 1755: when using an authkey, don't
|
||||
// show an authURL that might still be pending
|
||||
// from a previous non-completed interactive
|
||||
// login.
|
||||
return false
|
||||
}
|
||||
if upArgs.forceReauth && url == origAuthURL {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
return errors.New("--exit-node is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
prefs, err := prefsFromUpArgs(upArgs, warnf, st, runtime.GOOS)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
|
||||
if len(prefs.AdvertiseRoutes) > 0 {
|
||||
if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
|
||||
warnf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
curPrefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, os.Getenv("USER"))
|
||||
|
||||
if err := checkForAccidentalSettingReverts(upFlagSet, curPrefs, prefs, upCheckEnv{
|
||||
goos: runtime.GOOS,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}); err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL
|
||||
if controlURLChanged && st.BackendState == ipn.Running.String() && !upArgs.forceReauth {
|
||||
fatalf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
// If we're already running and none of the flags require a
|
||||
// restart, we can just do an EditPrefs call and change the
|
||||
// prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc)
|
||||
justEdit := st.BackendState == ipn.Running.String() &&
|
||||
!upArgs.forceReauth &&
|
||||
!upArgs.reset &&
|
||||
upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
|
||||
_, err := tailscale.EditPrefs(ctx, mp)
|
||||
return err
|
||||
}
|
||||
|
||||
// simpleUp is whether we're running a simple "tailscale up"
|
||||
// to transition to running from a previously-logged-in but
|
||||
// down state, without changing any settings.
|
||||
simpleUp := upFlagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
st.BackendState != ipn.NeedsLogin.String()
|
||||
|
||||
// At this point we need to subscribe to the IPN bus to watch
|
||||
// for state transitions and possible need to authenticate.
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
startingOrRunning := make(chan bool, 1) // gets value once starting or running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
printed := !simpleUp
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
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 [...]')"
|
||||
}
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case startingOrRunning <- true:
|
||||
default:
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
})
|
||||
// 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 pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
}
|
||||
|
||||
// Special case: bare "tailscale up" means to just start
|
||||
// running, if there's ever been a login.
|
||||
if simpleUp {
|
||||
_, err := tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
UpdatePrefs: prefs,
|
||||
}
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
// StateKey and send the prefs directly. Although the Windows
|
||||
// supports server mode, though, the transition to StateStore
|
||||
// is only half complete. Only server mode uses it, and the
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection identity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
// on our connection's identity.
|
||||
opts.StateKey = ""
|
||||
opts.Prefs = prefs
|
||||
}
|
||||
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
startLoginInteractive()
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Both these have the same ipn.Pref:
|
||||
addPrefFlagMapping("advertise-exit-node", "AdvertiseRoutes")
|
||||
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
||||
|
||||
// And this flag has two ipn.Prefs:
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
|
||||
|
||||
// The rest are 1:1:
|
||||
addPrefFlagMapping("accept-dns", "CorpDNS")
|
||||
addPrefFlagMapping("accept-routes", "RouteAll")
|
||||
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
|
||||
addPrefFlagMapping("host-routes", "AllowSingleHosts")
|
||||
addPrefFlagMapping("hostname", "Hostname")
|
||||
addPrefFlagMapping("login-server", "ControlURL")
|
||||
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
||||
addPrefFlagMapping("shields-up", "ShieldsUp")
|
||||
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
prefsOfFlag[flagName] = prefNames
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for _, pref := range prefNames {
|
||||
// Crash at runtime if there's a typo in the prefName.
|
||||
if _, ok := prefType.FieldByName(pref); !ok {
|
||||
panic(fmt.Sprintf("invalid ipn.Prefs field %q", pref))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preflessFlag reports whether flagName is a flag that doesn't
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
if preflessFlag(flagName) {
|
||||
return
|
||||
}
|
||||
if prefs, ok := prefsOfFlag[flagName]; ok {
|
||||
for _, pref := range prefs {
|
||||
reflect.ValueOf(mp).Elem().FieldByName(pref + "Set").SetBool(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||
}
|
||||
|
||||
const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires mentioning all\n" +
|
||||
"non-default flags. To proceed, either re-run your command with --reset or\n" +
|
||||
"use the command below to explicitly mention the current value of\n" +
|
||||
"all non-default settings:\n\n" +
|
||||
"\ttailscale up"
|
||||
|
||||
// upCheckEnv are extra parameters describing the environment as
|
||||
// needed by checkForAccidentalSettingReverts and friends.
|
||||
type upCheckEnv struct {
|
||||
goos string
|
||||
curExitNodeIP netaddr.IP
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
// people running "tailscale up" with a subset of the flags they
|
||||
// originally ran it with.
|
||||
//
|
||||
// For example, in Tailscale 1.6 and prior, a user might've advertised
|
||||
// a tag, but later tried to change just one other setting and forgot
|
||||
// to mention the tag later and silently wiped it out. We now
|
||||
// require --reset to change preferences to flag default values when
|
||||
// the flag is not mentioned on the command line.
|
||||
//
|
||||
// curPrefs is what's currently active on the server.
|
||||
//
|
||||
// mp is the mask of settings actually set, where mp.Prefs is the new
|
||||
// preferences to set, including any values set from implicit flags.
|
||||
func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs *ipn.Prefs, env upCheckEnv) error {
|
||||
if curPrefs.ControlURL == "" {
|
||||
// Don't validate things on initial "up" before a control URL has been set.
|
||||
return nil
|
||||
}
|
||||
|
||||
flagIsSet := map[string]bool{}
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
flagIsSet[f.Name] = true
|
||||
})
|
||||
|
||||
if len(flagIsSet) == 0 {
|
||||
// A bare "tailscale up" is a special case to just
|
||||
// mean bringing the network up without any changes.
|
||||
return nil
|
||||
}
|
||||
|
||||
// flagsCur is what flags we'd need to use to keep the exact
|
||||
// settings as-is.
|
||||
flagsCur := prefsToFlags(env, curPrefs)
|
||||
flagsNew := prefsToFlags(env, newPrefs)
|
||||
|
||||
var missing []string
|
||||
for flagName := range flagsCur {
|
||||
valCur, valNew := flagsCur[flagName], flagsNew[flagName]
|
||||
if flagIsSet[flagName] {
|
||||
continue
|
||||
}
|
||||
if reflect.DeepEqual(valCur, valNew) {
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(missing)
|
||||
|
||||
// Compute the stringification of the explicitly provided args in flagSet
|
||||
// to prepend to the command to run.
|
||||
var explicit []string
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
type isBool interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
if ib, ok := f.Value.(isBool); ok && ib.IsBoolFlag() {
|
||||
if f.Value.String() == "false" {
|
||||
explicit = append(explicit, "--"+f.Name+"=false")
|
||||
} else {
|
||||
explicit = append(explicit, "--"+f.Name)
|
||||
}
|
||||
} else {
|
||||
explicit = append(explicit, fmtFlagValueArg(f.Name, f.Value.String()))
|
||||
}
|
||||
})
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(accidentalUpPrefix)
|
||||
|
||||
for _, a := range append(explicit, missing...) {
|
||||
fmt.Fprintf(&sb, " %s", a)
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
|
||||
// this is just the operator user, which only needs to be set if it doesn't
|
||||
// match the current user.
|
||||
//
|
||||
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
|
||||
prefs.OperatorUser = oldPrefs.OperatorUser
|
||||
}
|
||||
}
|
||||
|
||||
func flagAppliesToOS(flag, goos string) bool {
|
||||
switch flag {
|
||||
case "netfilter-mode", "snat-subnet-routes":
|
||||
return goos == "linux"
|
||||
case "unattended":
|
||||
return goos == "windows"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interface{}) {
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
exitNodeIPStr := func() string {
|
||||
if !prefs.ExitNodeIP.IsZero() {
|
||||
return prefs.ExitNodeIP.String()
|
||||
}
|
||||
if prefs.ExitNodeID.IsZero() || env.curExitNodeIP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return env.curExitNodeIP.String()
|
||||
}
|
||||
|
||||
fs := newUpFlagSet(env.goos, new(upArgsT) /* dummy */)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
set := func(v interface{}) {
|
||||
if flagAppliesToOS(f.Name, env.goos) {
|
||||
ret[f.Name] = v
|
||||
} else {
|
||||
ret[f.Name] = nil
|
||||
}
|
||||
}
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
set(prefs.RouteAll)
|
||||
case "host-routes":
|
||||
set(prefs.AllowSingleHosts)
|
||||
case "accept-dns":
|
||||
set(prefs.CorpDNS)
|
||||
case "shields-up":
|
||||
set(prefs.ShieldsUp)
|
||||
case "exit-node":
|
||||
set(exitNodeIPStr())
|
||||
case "exit-node-allow-lan-access":
|
||||
set(prefs.ExitNodeAllowLANAccess)
|
||||
case "advertise-tags":
|
||||
set(strings.Join(prefs.AdvertiseTags, ","))
|
||||
case "hostname":
|
||||
set(prefs.Hostname)
|
||||
case "operator":
|
||||
set(prefs.OperatorUser)
|
||||
case "advertise-routes":
|
||||
var sb strings.Builder
|
||||
for i, r := range withoutExitNodes(prefs.AdvertiseRoutes) {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(r.String())
|
||||
}
|
||||
set(sb.String())
|
||||
case "advertise-exit-node":
|
||||
set(hasExitNodeRoutes(prefs.AdvertiseRoutes))
|
||||
case "snat-subnet-routes":
|
||||
set(!prefs.NoSNAT)
|
||||
case "netfilter-mode":
|
||||
set(prefs.NetfilterMode.String())
|
||||
case "unattended":
|
||||
set(prefs.ForceDaemon)
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func fmtFlagValueArg(flagName string, val interface{}) string {
|
||||
if val == true {
|
||||
return "--" + flagName
|
||||
}
|
||||
if val == "" {
|
||||
return "--" + flagName + "="
|
||||
}
|
||||
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(fmt.Sprint(val)))
|
||||
}
|
||||
|
||||
func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
|
||||
var v4, v6 bool
|
||||
for _, r := range rr {
|
||||
if r.Bits() == 0 {
|
||||
if r.IP().Is4() {
|
||||
v4 = true
|
||||
} else if r.IP().Is6() {
|
||||
v6 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return v4 && v6
|
||||
}
|
||||
|
||||
// withoutExitNodes returns rr unchanged if it has only 1 or 0 /0
|
||||
// routes. If it has both IPv4 and IPv6 /0 routes, then it returns
|
||||
// a copy with all /0 routes removed.
|
||||
func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
|
||||
if !hasExitNodeRoutes(rr) {
|
||||
return rr
|
||||
}
|
||||
var out []netaddr.IPPrefix
|
||||
for _, r := range rr {
|
||||
if r.Bits() > 0 {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// exitNodeIP returns the exit node IP from p, using st to map
|
||||
// it from its ID form to an IP address if needed.
|
||||
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netaddr.IP) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !p.ExitNodeIP.IsZero() {
|
||||
return p.ExitNodeIP
|
||||
}
|
||||
id := p.ExitNodeID
|
||||
if id.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, p := range st.Peer {
|
||||
if p.ID == id {
|
||||
if len(p.TailscaleIPs) > 0 {
|
||||
return p.TailscaleIPs[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,327 +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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
//go:embed web.html
|
||||
var webHTML string
|
||||
|
||||
//go:embed web.css
|
||||
var webCSS string
|
||||
|
||||
var tmpl *template.Template
|
||||
|
||||
func init() {
|
||||
tmpl = template.Must(template.New("web.html").Parse(webHTML))
|
||||
template.Must(tmpl.New("web.css").Parse(webCSS))
|
||||
}
|
||||
|
||||
type tmplData struct {
|
||||
Profile tailcfg.UserProfile
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
Name: "web",
|
||||
ShortUsage: "web [flags]",
|
||||
ShortHelp: "Run a web server for controlling Tailscale",
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
return webf
|
||||
})(),
|
||||
Exec: runWeb,
|
||||
}
|
||||
|
||||
var webArgs struct {
|
||||
listen string
|
||||
cgi bool
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
|
||||
log.Printf("tailscale.cgi: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
|
||||
func auth() (string, error) {
|
||||
if distro.Get() == distro.Synology {
|
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: %v: %s", err, out)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
if distro.Get() != distro.Synology {
|
||||
return false
|
||||
}
|
||||
if r.Header.Get("X-Syno-Token") != "" {
|
||||
return false
|
||||
}
|
||||
if r.URL.Query().Get("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
// We need a SynoToken for authenticate.cgi.
|
||||
// So we tell the client to get one.
|
||||
serverURL := r.URL.Scheme + "://" + r.URL.Host
|
||||
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
|
||||
return true
|
||||
}
|
||||
|
||||
const synoTokenRedirectHTML = `<html><body>
|
||||
Redirecting with session token...
|
||||
<script>
|
||||
var serverURL = %q;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("GET", serverURL + "/webman/login.cgi", true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
var token = jsonResponse["SynoToken"];
|
||||
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token;
|
||||
};
|
||||
req.send(null);
|
||||
</script>
|
||||
</body></html>
|
||||
`
|
||||
|
||||
const authenticationRedirectHTML = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting...</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: rgb(249, 247, 246);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin-bottom: 2rem;
|
||||
border: 4px rgba(112, 110, 109, 0.5) solid;
|
||||
border-left-color: transparent;
|
||||
border-radius: 9999px;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
-webkit-animation: spin 700ms linear infinite;
|
||||
animation: spin 800ms linear infinite;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgb(112, 110, 109);
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="spinner"></div>
|
||||
<div class="label">Redirecting...</div>
|
||||
</body>
|
||||
`
|
||||
|
||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if synoTokenRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
|
||||
w.Write([]byte(authenticationRedirectHTML))
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(mi{"error": err})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
return
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
profile := st.User[st.Self.UserID]
|
||||
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
||||
data := tmplData{
|
||||
SynologyUser: user,
|
||||
Profile: profile,
|
||||
Status: st.BackendState,
|
||||
DeviceName: deviceName,
|
||||
}
|
||||
if len(st.TailscaleIPs) != 0 {
|
||||
data.IP = st.TailscaleIPs[0].String()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpl.Execute(buf, data); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't fetch status: %v", err)
|
||||
}
|
||||
origAuthURL := st.AuthURL
|
||||
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
return url != origAuthURL
|
||||
}
|
||||
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
go pump(pumpCtx, bc, c)
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.Engine != nil {
|
||||
select {
|
||||
case gotEngineUpdate <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
retErr = fmt.Errorf("backend error: %v", msg)
|
||||
cancel()
|
||||
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
// we're subscribed to updates. Otherwise we can miss
|
||||
// an update upon its transition to running. Do so by causing some traffic
|
||||
// back to the bus that we then wait on.
|
||||
bc.RequestEngineStatus()
|
||||
select {
|
||||
case <-gotEngineUpdate:
|
||||
case <-pumpCtx.Done():
|
||||
return authURL, pumpCtx.Err()
|
||||
}
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
bc.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
})
|
||||
bc.StartLoginInteractive()
|
||||
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
return authURL, retErr
|
||||
}
|
||||
@@ -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>
|
||||
@@ -2,14 +2,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
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
|
||||
W 💣 github.com/github/certstore from tailscale.com/control/controlclient
|
||||
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/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
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
|
||||
@@ -20,10 +20,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
W github.com/pkg/errors from github.com/github/certstore
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/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
|
||||
@@ -32,59 +31,53 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/link/channel+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/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
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
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/derp/derpmap from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/internal/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/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
|
||||
@@ -92,65 +85,51 @@ 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/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/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/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/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+
|
||||
W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/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/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wglog 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
|
||||
@@ -166,35 +145,36 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/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 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+
|
||||
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/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
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
|
||||
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/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/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -221,7 +201,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
|
||||
L embed from tailscale.com/net/dns
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -236,25 +215,24 @@ 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/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
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+
|
||||
@@ -277,6 +255,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
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if strings.HasSuffix(os.Args[0], "tailscaled") {
|
||||
tailscaled_main()
|
||||
} else if strings.HasSuffix(os.Args[0], "tailscale") {
|
||||
tailscale_main()
|
||||
} else {
|
||||
panic(os.Args[0])
|
||||
}
|
||||
}
|
||||
@@ -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,28 +21,18 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"inet.af/netaddr"
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -65,43 +53,22 @@ 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 {
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string // tun name, "userspace-networking", or comma-separated list thereof
|
||||
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 tailscaled_main() {
|
||||
func main() {
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
// allocations. So let's be aggressive about garbage collection,
|
||||
@@ -113,31 +80,17 @@ func tailscaled_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()
|
||||
@@ -150,22 +103,11 @@ func tailscaled_main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -183,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)
|
||||
@@ -200,7 +132,6 @@ func run() error {
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
dns.Cleanup(logf, args.tunname)
|
||||
router.Cleanup(logf, args.tunname)
|
||||
return nil
|
||||
}
|
||||
@@ -215,68 +146,20 @@ func run() error {
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
}
|
||||
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)
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
var impl wgengine.FakeImplFunc
|
||||
if args.tunname == "userspace-networking" {
|
||||
impl = netstack.Impl
|
||||
}
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
|
||||
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 := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex // guards the following field
|
||||
dns netstack.DNSMap
|
||||
)
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
useNetstackForIP := func(ip netaddr.IP) bool {
|
||||
// TODO(bradfitz): this isn't exactly right.
|
||||
// We should also support subnets when the
|
||||
// prefs are configured as such.
|
||||
return tsaddr.IsTailscaleIP(ip)
|
||||
}
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ns != nil && useNetstackForIP(ipp.IP()) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -303,7 +186,8 @@ func run() error {
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
SurviveDisconnects: runtime.GOOS != "windows",
|
||||
LegacyConfigPath: paths.LegacyConfigPath(),
|
||||
SurviveDisconnects: true,
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
@@ -316,80 +200,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
|
||||
r, err := router.New(logf, dev)
|
||||
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)
|
||||
@@ -409,18 +219,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,15 +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.
|
||||
|
||||
// +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,278 +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"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tempfork/wireguard-windows/firewall"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"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])
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
guid, err := windows.GUIDFromString(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
|
||||
}
|
||||
|
||||
luid, err := winipcfg.LUIDFromGUID(&guid)
|
||||
if err != nil {
|
||||
log.Fatalf("no interface with GUID %q", guid)
|
||||
}
|
||||
|
||||
noProtection := false
|
||||
var dnsIPs []net.IP // unused in called code.
|
||||
start := time.Now()
|
||||
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
|
||||
log.Printf("killswitch enabled, took %s", time.Since(start))
|
||||
|
||||
// Block until the monitor goroutine shuts us down.
|
||||
select {}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
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, opts)
|
||||
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
|
||||
}
|
||||
@@ -59,11 +59,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
|
||||
@@ -75,13 +75,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{
|
||||
|
||||
@@ -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"
|
||||
"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,8 +114,6 @@ 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
|
||||
|
||||
@@ -75,8 +137,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 +146,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 +158,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,29 +169,18 @@ 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
|
||||
@@ -146,7 +197,7 @@ 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()
|
||||
}
|
||||
@@ -156,7 +207,7 @@ func (c *Auto) Start() {
|
||||
// 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() {
|
||||
func (c *Client) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
@@ -195,7 +246,7 @@ func (c *Auto) sendNewMapRequest() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelAuth() {
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
c.authCancel()
|
||||
@@ -206,7 +257,7 @@ func (c *Auto) cancelAuth() {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapLocked() {
|
||||
func (c *Client) cancelMapLocked() {
|
||||
if c.mapCancel != nil {
|
||||
c.mapCancel()
|
||||
}
|
||||
@@ -215,13 +266,13 @@ 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()
|
||||
|
||||
@@ -257,7 +308,7 @@ func (c *Auto) cancelMapSafely() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) authRoutine() {
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||
|
||||
@@ -268,7 +319,7 @@ func (c *Auto) authRoutine() {
|
||||
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)
|
||||
c.logf("authRoutine: %s; goal=nil", c.state)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -298,7 +349,6 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(ctx)
|
||||
goal.sendLogoutError(err)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -338,10 +388,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 +425,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 +433,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)
|
||||
|
||||
@@ -459,10 +508,8 @@ 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 {
|
||||
@@ -495,7 +542,6 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
})
|
||||
|
||||
health.SetInPollNetMap(false)
|
||||
c.mu.Lock()
|
||||
c.synced = false
|
||||
c.inPollNetMap = false
|
||||
@@ -520,24 +566,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,12 +587,13 @@ 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()
|
||||
}
|
||||
|
||||
func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
if ni == nil {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
@@ -563,7 +606,7 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
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
|
||||
@@ -575,7 +618,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *persist.Persist
|
||||
var p *Persist
|
||||
var fin *empty.Message
|
||||
if state == StateAuthenticated {
|
||||
fin = new(empty.Message)
|
||||
@@ -608,7 +651,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()
|
||||
@@ -622,57 +665,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()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) Shutdown() {
|
||||
func (c *Client) Shutdown() {
|
||||
c.logf("client.Shutdown()")
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -686,7 +698,6 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
|
||||
if !closed {
|
||||
c.unregisterHealthWatch()
|
||||
close(c.quit)
|
||||
c.cancelAuth()
|
||||
<-c.authDone
|
||||
@@ -698,17 +709,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() wgkey.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()
|
||||
}
|
||||
|
||||
@@ -1,77 +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)
|
||||
}
|
||||
@@ -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", "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)
|
||||
|
||||
@@ -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 controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
buf := make([]byte, 1<<20)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
|
||||
if string(in) == "0x0" {
|
||||
return in
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
return v
|
||||
}
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
if err != nil {
|
||||
return []byte("??")
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[string(in)] = v
|
||||
return v
|
||||
})
|
||||
}
|
||||
@@ -1,11 +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())
|
||||
}
|
||||
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,14 +5,94 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
|
||||
return &tailcfg.Node{ID: id, Name: name}
|
||||
}
|
||||
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")),
|
||||
},
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
@@ -22,13 +102,7 @@ func TestNewDirect(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
},
|
||||
}
|
||||
opts := Options{ServerURL: "https://example.com", MachinePrivateKey: key, Hostinfo: hi}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -63,7 +137,7 @@ func TestNewDirect(t *testing.T) {
|
||||
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
|
||||
}
|
||||
|
||||
endpoints := fakeEndpoints(1, 2, 3)
|
||||
endpoints := []string{"1", "2", "3"}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(12) want true got %v", changed)
|
||||
@@ -76,30 +150,9 @@ func TestNewDirect(t *testing.T) {
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
endpoints = fakeEndpoints(4, 5, 6)
|
||||
endpoints = []string{"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),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
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 format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -59,9 +58,6 @@ func osVersionLinux() string {
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
if inKnative() {
|
||||
attrBuf.WriteString("; env=kn")
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
|
||||
id := m["ID"]
|
||||
@@ -103,21 +99,5 @@ func inContainer() (ret bool) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
lineread.File("/proc/mounts", func(line []byte) error {
|
||||
if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
|
||||
ret = true
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func inKnative() bool {
|
||||
// https://cloud.google.com/run/docs/reference/container-contract#env-vars
|
||||
if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
|
||||
os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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,282 +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"
|
||||
"sort"
|
||||
|
||||
"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 = 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
|
||||
}
|
||||
@@ -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,26 +2,25 @@
|
||||
// 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"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"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
|
||||
|
||||
@@ -31,8 +30,8 @@ type NetworkMap struct {
|
||||
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 []netaddr.IPPrefix
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
@@ -57,8 +56,11 @@ 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
|
||||
@@ -247,8 +249,126 @@ 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: wgcfg.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 = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:])
|
||||
} 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.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: not accepting default route from %q (%v)",
|
||||
nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",
|
||||
allowedIP, nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func nodeDebugName(n *tailcfg.Node) string {
|
||||
name := n.Name
|
||||
if name == "" {
|
||||
name = n.Hostinfo.Hostname
|
||||
}
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[:i]
|
||||
}
|
||||
if name == "" && len(n.Addresses) != 0 {
|
||||
return n.Addresses[0].String()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// 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 netaddr.IPPrefix) bool {
|
||||
if cidr.Bits == 0 {
|
||||
return false
|
||||
}
|
||||
if !cidr.IsSingleIP() {
|
||||
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
|
||||
}
|
||||
_, port, err := net.SplitHostPort(epStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
_, err = strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
if peer.Endpoints != "" {
|
||||
peer.Endpoints += ","
|
||||
}
|
||||
peer.Endpoints += epStr
|
||||
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 {
|
||||
@@ -2,10 +2,11 @@
|
||||
// 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"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -250,7 +251,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: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,7 +264,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: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -282,3 +283,15 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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"
|
||||
@@ -11,15 +11,6 @@ import (
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
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) {
|
||||
@@ -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,181 +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.
|
||||
|
||||
// +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/github/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,17 +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.
|
||||
|
||||
// +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,103 +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
|
||||
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.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)
|
||||
}
|
||||
@@ -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,14 +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
|
||||
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
|
||||
@@ -49,9 +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
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -66,12 +64,6 @@ 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 {
|
||||
@@ -85,14 +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,
|
||||
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 {
|
||||
@@ -156,10 +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
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -168,9 +155,8 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -252,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.
|
||||
@@ -345,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
|
||||
@@ -436,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")
|
||||
@@ -466,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
@@ -409,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)
|
||||
@@ -792,63 +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 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) })
|
||||
|
||||
@@ -63,7 +63,6 @@ type Client struct {
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
@@ -334,11 +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),
|
||||
)
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -647,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) {
|
||||
@@ -746,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -83,9 +83,6 @@ func Prod() *tailcfg.DERPMap {
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
11: derpRegion(11, "sao", "São Paulo",
|
||||
derpNode("a", "18.230.97.74", "2600:1f1e:ee4:5611:ec5c:1736:d43b:a454"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +147,9 @@ const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP().As16()
|
||||
a := ipp.IP.As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port())
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
@@ -164,9 +164,10 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPortFrom(
|
||||
netaddr.IPFrom16(a),
|
||||
binary.BigEndian.Uint16(p[16:18])))
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
@@ -186,9 +187,9 @@ const pongLen = 12 + 16 + 2
|
||||
func (m *Pong) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
|
||||
d = d[copy(d, m.TxID[:]):]
|
||||
ip16 := m.Src.IP().As16()
|
||||
ip16 := m.Src.IP.As16()
|
||||
d = d[copy(d, ip16[:]):]
|
||||
binary.BigEndian.PutUint16(d, m.Src.Port())
|
||||
binary.BigEndian.PutUint16(d, m.Src.Port)
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -200,10 +201,10 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
|
||||
copy(m.TxID[:], p)
|
||||
p = p[12:]
|
||||
|
||||
srcIP, _ := netaddr.FromStdIP(net.IP(p[:16]))
|
||||
m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16]))
|
||||
p = p[16:]
|
||||
port := binary.BigEndian.Uint16(p)
|
||||
m.Src = netaddr.IPPortFrom(srcIP, port)
|
||||
|
||||
m.Src.Port = binary.BigEndian.Uint16(p)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +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.
|
||||
// +build gofuzz
|
||||
|
||||
package disco
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
m, _ := Parse(data)
|
||||
|
||||
newBytes := m.AppendMarshal(data)
|
||||
parsedMarshall, _ := Parse(newBytes)
|
||||
|
||||
if m != parsedMarshall {
|
||||
panic("Parsing error")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
42
go.mod
42
go.mod
@@ -1,50 +1,44 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.16
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/frankban/quicktest v1.12.1
|
||||
github.com/github/certstore v0.1.0
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/google/go-cmp v0.5.5
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.3.2
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.1.0
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2
|
||||
|
||||
481
go.sum
481
go.sum
@@ -1,7 +1,34 @@
|
||||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.52.1-0.20200122224058-0482b626c726/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
@@ -9,191 +36,403 @@ github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEK
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
||||
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
|
||||
github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
|
||||
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 h1:TGPWAij+nY2FB7TlyUTqTmYvXJon/AZAfRMYc/76K80=
|
||||
github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2/go.mod h1:Sgb3YVYOB2iCO06NJ6We5gjXe7uxxM3zPYoEXjuTKno=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/dpjacques/clockwork v0.1.1-0.20200827220843-c1f524b839be/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c=
|
||||
github.com/frankban/quicktest v1.12.1/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github/v28 v28.1.2-0.20191108005307-e555eab49ce8/go.mod h1:g82e6OHbJ0WYrYeOrid1MMfHAtqjxBz+N74tfAt9KrQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1 h1:zc0R6cOw98cMengLA0fvU55mqbnN7sd/tBMLzSejp+M=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.3.2 h1:fMZOU2/M7PRMzGM3br5l1N2fu6bPSHtRytmQ338a9iA=
|
||||
github.com/mdlayher/netlink v1.3.2/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339 h1:OjLaZ57xeWJUUBAJN5KmsgjsaUABTZhcvgO/lKtZ8sQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df h1:ekBw6cxmDhXf9YxTmMZh7SPwUh9rnRRnaoX7HFiGobc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d h1:qJSz1zlpuPLmfACtnj+tAH4g3iasJMBW8dpeFm5f4wg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -201,37 +440,49 @@ golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -241,34 +492,84 @@ golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ5
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b h1:jEdfCm+8YTWSYgU4L7Nq0jjU+q9RxIhi0cXLTY+Ih3A=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6 h1:H5EvGkFG+pgAAbZMV8Me3Gy+HUYdaDcGXKWWixZ0EE8=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6/go.mod h1:5DEMKRjYDiM24fvDUWPjBpABm9ROMcv/kEcox3fHtm0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d h1:9tuJMxDV7THGfXWirKBD/v9rbsBC21bHd2eEYsYuIek=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c h1:rzDy/tC8LjEdN94+i0Bu22tTo/qE9cvhKyfD0HMU0NU=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841 h1:2HpK+rC0Arcu98JukIlyVfEaE2OsvtmBFc8rs/2SJYs=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4 h1:g1VVXY1xRKoO17aKY3g9KeJxDW0lGx1n2Y+WPSWkOL8=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4/go.mod h1:56/0QVlZ4NmPRh1QuU2OfrKqjSgt5P39R534gD2JMpQ=
|
||||
inet.af/wf v0.0.0-20210515021317-09f8efa8ac30 h1:TLxVVv7rmErJW7l81tbbR2BkOIYBI3YdxbJbEs/HJt8=
|
||||
inet.af/wf v0.0.0-20210515021317-09f8efa8ac30/go.mod h1:ViGMZRA6+RA318D7GCncrjv5gHUrPYrNDejjU12tikA=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756 h1:muIT3C1rH3/xpvIH8blKkMvhctV7F+OtZqs7kcwHDBQ=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756/go.mod h1:ViGMZRA6+RA318D7GCncrjv5gHUrPYrNDejjU12tikA=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
|
||||
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
|
||||
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
||||
351
health/health.go
351
health/health.go
@@ -1,351 +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 health is a registry for other packages to report & check
|
||||
// overall health status of the node.
|
||||
package health
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
// mu guards everything in this var block.
|
||||
mu sync.Mutex
|
||||
|
||||
sysErr = map[Subsystem]error{} // error key => err (or nil for no error)
|
||||
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
|
||||
timer *time.Timer
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
lastMapPollEndedAt time.Time
|
||||
lastStreamedMapResponse time.Time
|
||||
derpHomeRegion int
|
||||
derpRegionConnected = map[int]bool{}
|
||||
derpRegionLastFrame = map[int]time.Time{}
|
||||
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
|
||||
ipnState string
|
||||
ipnWantRunning bool
|
||||
anyInterfaceUp = true // until told otherwise
|
||||
udp4Unbound bool
|
||||
)
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
type Subsystem string
|
||||
|
||||
const (
|
||||
// SysOverall is the name representing the overall health of
|
||||
// the system, rather than one particular subsystem.
|
||||
SysOverall = Subsystem("overall")
|
||||
|
||||
// SysRouter is the name of the wgengine/router subsystem.
|
||||
SysRouter = Subsystem("router")
|
||||
|
||||
// SysDNS is the name of the net/dns subsystem.
|
||||
SysDNS = Subsystem("dns")
|
||||
|
||||
// SysNetworkCategory is the name of the subsystem that sets
|
||||
// the Windows network adapter's "category" (public, private, domain).
|
||||
// If it's unhealthy, the Windows firewall rules won't match.
|
||||
SysNetworkCategory = Subsystem("network-category")
|
||||
)
|
||||
|
||||
type watchHandle byte
|
||||
|
||||
// RegisterWatcher adds a function that will be called if an
|
||||
// error changes state either to unhealthy or from unhealthy. It is
|
||||
// not called on transition from unknown to healthy. It must be non-nil
|
||||
// and is run in its own goroutine. The returned func unregisters it.
|
||||
func RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
handle := new(watchHandle)
|
||||
watchers[handle] = cb
|
||||
if timer == nil {
|
||||
timer = time.AfterFunc(time.Minute, timerSelfCheck)
|
||||
}
|
||||
return func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
delete(watchers, handle)
|
||||
if len(watchers) == 0 && timer != nil {
|
||||
timer.Stop()
|
||||
timer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetRouterHealth sets the state of the wgengine/router.Router.
|
||||
func SetRouterHealth(err error) { set(SysRouter, err) }
|
||||
|
||||
// RouterHealth returns the wgengine/router.Router error state.
|
||||
func RouterHealth() error { return get(SysRouter) }
|
||||
|
||||
// SetDNSHealth sets the state of the net/dns.Manager
|
||||
func SetDNSHealth(err error) { set(SysDNS, err) }
|
||||
|
||||
// DNSHealth returns the net/dns.Manager error state.
|
||||
func DNSHealth() error { return get(SysDNS) }
|
||||
|
||||
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
|
||||
// This only applies on Windows.
|
||||
func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
|
||||
|
||||
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
|
||||
|
||||
func get(key Subsystem) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return sysErr[key]
|
||||
}
|
||||
|
||||
func set(key Subsystem, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
setLocked(key, err)
|
||||
}
|
||||
|
||||
func setLocked(key Subsystem, err error) {
|
||||
old, ok := sysErr[key]
|
||||
if !ok && err == nil {
|
||||
// Initial happy path.
|
||||
sysErr[key] = nil
|
||||
selfCheckLocked()
|
||||
return
|
||||
}
|
||||
if ok && (old == nil) == (err == nil) {
|
||||
// No change in overall error status (nil-vs-not), so
|
||||
// don't run callbacks, but exact error might've
|
||||
// changed, so note it.
|
||||
if err != nil {
|
||||
sysErr[key] = err
|
||||
}
|
||||
return
|
||||
}
|
||||
sysErr[key] = err
|
||||
selfCheckLocked()
|
||||
for _, cb := range watchers {
|
||||
go cb(key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse
|
||||
// message in streaming mode, even if it's just a keep-alive message.
|
||||
func GotStreamedMapResponse() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lastStreamedMapResponse = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetInPollNetMap records that we're in
|
||||
func SetInPollNetMap(v bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if v == inMapPoll {
|
||||
return
|
||||
}
|
||||
inMapPoll = v
|
||||
if v {
|
||||
inMapPollSince = time.Now()
|
||||
} else {
|
||||
lastMapPollEndedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
|
||||
func SetMagicSockDERPHome(region int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpHomeRegion = region
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// NoteMapRequestHeard notes whenever we successfully sent a map request
|
||||
// to control for which we received a 200 response.
|
||||
func NoteMapRequestHeard(mr *tailcfg.MapRequest) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
// TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare
|
||||
// against SetMagicSockDERPHome and
|
||||
// SetDERPRegionConnectedState
|
||||
|
||||
lastMapRequestHeard = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func SetDERPRegionConnectedState(region int, connected bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpRegionConnected[region] = connected
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func NoteDERPRegionReceivedFrame(region int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpRegionLastFrame[region] = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
|
||||
func SetIPNState(state string, wantRunning bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
ipnState = state
|
||||
ipnWantRunning = wantRunning
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetAnyInterfaceUp sets whether any network interface is up.
|
||||
func SetAnyInterfaceUp(up bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
anyInterfaceUp = up
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetUDP4Unbound sets whether the udp4 bind failed completely.
|
||||
func SetUDP4Unbound(unbound bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
udp4Unbound = unbound
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func timerSelfCheck() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
checkReceiveFuncs()
|
||||
selfCheckLocked()
|
||||
if timer != nil {
|
||||
timer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func selfCheckLocked() {
|
||||
if ipnState == "" {
|
||||
// Don't check yet.
|
||||
return
|
||||
}
|
||||
setLocked(SysOverall, overallErrorLocked())
|
||||
}
|
||||
|
||||
func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
return errors.New("network down")
|
||||
}
|
||||
if ipnState != "Running" || !ipnWantRunning {
|
||||
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning)
|
||||
}
|
||||
now := time.Now()
|
||||
if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) {
|
||||
return errors.New("not in map poll")
|
||||
}
|
||||
const tooIdle = 2*time.Minute + 5*time.Second
|
||||
if d := now.Sub(lastStreamedMapResponse).Round(time.Second); d > tooIdle {
|
||||
return fmt.Errorf("no map response in %v", d)
|
||||
}
|
||||
rid := derpHomeRegion
|
||||
if rid == 0 {
|
||||
return errors.New("no DERP home")
|
||||
}
|
||||
if !derpRegionConnected[rid] {
|
||||
return fmt.Errorf("not connected to home DERP region %v", rid)
|
||||
}
|
||||
if d := now.Sub(derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle {
|
||||
return fmt.Errorf("haven't heard from home DERP region %v in %v", rid, d)
|
||||
}
|
||||
if udp4Unbound {
|
||||
return errors.New("no udp4 bind")
|
||||
}
|
||||
|
||||
// TODO: use
|
||||
_ = inMapPollSince
|
||||
_ = lastMapPollEndedAt
|
||||
_ = lastStreamedMapResponse
|
||||
_ = lastMapRequestHeard
|
||||
|
||||
var errs []error
|
||||
for _, recv := range receiveFuncs {
|
||||
if recv.missing {
|
||||
errs = append(errs, fmt.Errorf("%s is not running", recv.name))
|
||||
}
|
||||
}
|
||||
for sys, err := range sysErr {
|
||||
if err == nil || sys == SysOverall {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("%v: %w", sys, err))
|
||||
}
|
||||
sort.Slice(errs, func(i, j int) bool {
|
||||
// Not super efficient (stringifying these in a sort), but probably max 2 or 3 items.
|
||||
return errs[i].Error() < errs[j].Error()
|
||||
})
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
var (
|
||||
ReceiveIPv4 = ReceiveFuncStats{name: "ReceiveIPv4"}
|
||||
ReceiveIPv6 = ReceiveFuncStats{name: "ReceiveIPv6"}
|
||||
ReceiveDERP = ReceiveFuncStats{name: "ReceiveDERP"}
|
||||
|
||||
receiveFuncs = []*ReceiveFuncStats{&ReceiveIPv4, &ReceiveIPv6, &ReceiveDERP}
|
||||
)
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
name string
|
||||
// numCalls is the number of times the receive func has ever been called.
|
||||
// It is required because it is possible for a receive func's wireguard-go goroutine
|
||||
// to be active even though the receive func isn't.
|
||||
// The wireguard-go goroutine alternates between calling the receive func and
|
||||
// processing what the func returned.
|
||||
numCalls uint64 // accessed atomically
|
||||
// prevNumCalls is the value of numCalls last time the health check examined it.
|
||||
prevNumCalls uint64
|
||||
// inCall indicates whether the receive func is currently running.
|
||||
inCall uint32 // bool, accessed atomically
|
||||
// missing indicates whether the receive func is not running.
|
||||
missing bool
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Enter() {
|
||||
atomic.AddUint64(&s.numCalls, 1)
|
||||
atomic.StoreUint32(&s.inCall, 1)
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Exit() {
|
||||
atomic.StoreUint32(&s.inCall, 0)
|
||||
}
|
||||
|
||||
func checkReceiveFuncs() {
|
||||
for _, recv := range receiveFuncs {
|
||||
recv.missing = false
|
||||
prev := recv.prevNumCalls
|
||||
numCalls := atomic.LoadUint64(&recv.numCalls)
|
||||
recv.prevNumCalls = numCalls
|
||||
if numCalls > prev {
|
||||
// OK: the function has gotten called since last we checked
|
||||
continue
|
||||
}
|
||||
if atomic.LoadUint32(&recv.inCall) == 1 {
|
||||
// OK: the function is active, probably blocked due to inactivity
|
||||
continue
|
||||
}
|
||||
// Not OK: The function is not active, and not accumulating new calls.
|
||||
// It is probably MIA.
|
||||
recv.missing = true
|
||||
}
|
||||
}
|
||||
@@ -1,174 +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 deephash hashes a Go value recursively, in a predictable
|
||||
// order, without looping.
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
// 64 matches the chunk size in crypto/sha256/sha256.go
|
||||
b := bufio.NewWriterSize(h, 64)
|
||||
Print(b, v)
|
||||
b.Flush()
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w *bufio.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
var (
|
||||
netaddrIPType = reflect.TypeOf(netaddr.IP{})
|
||||
netaddrIPPrefix = reflect.TypeOf(netaddr.IPPrefix{})
|
||||
wgkeyKeyType = reflect.TypeOf(wgkey.Key{})
|
||||
wgkeyPrivateType = reflect.TypeOf(wgkey.Private{})
|
||||
tailcfgDiscoKeyType = reflect.TypeOf(tailcfg.DiscoKey{})
|
||||
)
|
||||
|
||||
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Special case some common types.
|
||||
if v.CanInterface() {
|
||||
switch v.Type() {
|
||||
case netaddrIPType:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case netaddrIPPrefix:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case wgkeyKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Key)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Key)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case wgkeyPrivateType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Private)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Private)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case tailcfgDiscoKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generic handling.
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
w.WriteString("struct{\n")
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Field(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
w.WriteString(": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.String:
|
||||
w.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
@@ -1,72 +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 deephash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
// v contains the types of values we care about for our current callers.
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
hash1 := Hash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
hash2 := Hash(getVal())
|
||||
if hash1 != hash2 {
|
||||
t.Error("second hash didn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPFrom16([16]byte{3: 3}), 5)},
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: wgcfg.Endpoints{
|
||||
IPPorts: wgcfg.NewIPPortSet(netaddr.MustParseIPPort("42.42.42.42:5")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
Routes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("1.2.3.0/24"),
|
||||
netaddr.MustParseIPPrefix("1234::/64"),
|
||||
},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IP{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("4.3.2.1")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("9.9.9.9")},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IPPort{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIPPort("1.2.3.4:11"), netaddr.MustParseIPPort("4.3.2.1:22")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIPPort("8.8.8.8:11"), netaddr.MustParseIPPort("9.9.9.9:22")},
|
||||
},
|
||||
map[tailcfg.DiscoKey]bool{
|
||||
{1: 1}: true,
|
||||
{1: 2}: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHash(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Hash(v)
|
||||
}
|
||||
}
|
||||
103
internal/deepprint/deepprint.go
Normal file
103
internal/deepprint/deepprint.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 deepprint walks a Go value recursively, in a predictable
|
||||
// order, without looping, and prints each value out to a given
|
||||
// Writer, which is assumed to be a hash.Hash, as this package doesn't
|
||||
// format things nicely.
|
||||
//
|
||||
// This is intended as a lighter version of go-spew, etc. We don't need its
|
||||
// features when our writer is just a hash.
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
Print(h, v)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
fmt.Fprintf(w, "struct{\n")
|
||||
t := v.Type()
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
fmt.Fprintf(w, "%s: ", sf.Name)
|
||||
print(w, v.Field(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
fmt.Fprintf(w, ": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, "%s", v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
66
internal/deepprint/deepprint_test.go
Normal file
66
internal/deepprint/deepprint_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 deepprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
// v contains the types of values we care about for our current callers.
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
var buf bytes.Buffer
|
||||
Print(&buf, v)
|
||||
t.Logf("Got: %s", buf.Bytes())
|
||||
|
||||
hash1 := Hash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
hash2 := Hash(getVal())
|
||||
if hash1 != hash2 {
|
||||
t.Error("second hash didn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: "foo:5",
|
||||
},
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
DNS: dns.Config{
|
||||
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
|
||||
Domains: []string{"tailscale.net"},
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
"key3": "val3",
|
||||
"key4": "val4",
|
||||
"key5": "val5",
|
||||
"key6": "val6",
|
||||
"key7": "val7",
|
||||
"key8": "val8",
|
||||
"key9": "val9",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
// This is a slightly modified fork of Go's src/internal/fmtsort/sort.go
|
||||
|
||||
package deephash
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
161
ipn/backend.go
161
ipn/backend.go
@@ -5,15 +5,16 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
type State int
|
||||
@@ -28,7 +29,7 @@ const (
|
||||
Running
|
||||
)
|
||||
|
||||
// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
|
||||
// GoogleIDToken Type is the oauth2.Token.TokenType for the Google
|
||||
// ID tokens used by the Android client.
|
||||
const GoogleIDTokenType = "ts_android_google_login"
|
||||
|
||||
@@ -45,10 +46,10 @@ func (s State) String() string {
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
type EngineStatus struct {
|
||||
RBytes, WBytes int64
|
||||
RBytes, WBytes wgengine.ByteCount
|
||||
NumLive int
|
||||
LiveDERPs int // number of active DERP connections
|
||||
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
|
||||
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
|
||||
}
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -57,33 +58,18 @@ type EngineStatus struct {
|
||||
// that they have not changed.
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
|
||||
// ErrMessage, if non-nil, contains a critical error message.
|
||||
// For State InUseOtherUser, ErrMessage is not critical and just contains the details.
|
||||
ErrMessage *string
|
||||
|
||||
LoginFinished *empty.Message // non-nil when/if the login process succeeded
|
||||
State *State // if non-nil, the new or current IPN state
|
||||
Prefs *Prefs // if non-nil, the new or current preferences
|
||||
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
|
||||
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
|
||||
BrowseToURL *string // if non-nil, UI should open a browser right now
|
||||
BackendLogID *string // if non-nil, the public logtail ID used by backend
|
||||
PingResult *ipnstate.PingResult // if non-nil, a ping response arrived
|
||||
|
||||
// FilesWaiting if non-nil means that files are buffered in
|
||||
// the Tailscale daemon and ready for local transfer to the
|
||||
// user's preferred storage location.
|
||||
FilesWaiting *empty.Message `json:",omitempty"`
|
||||
|
||||
// IncomingFiles, if non-nil, specifies which files are in the
|
||||
// process of being received. A nil IncomingFiles means this
|
||||
// Notify should not update the state of file transfers. A non-nil
|
||||
// but empty IncomingFiles means that no files are in the middle
|
||||
// of being transferred.
|
||||
IncomingFiles []PartialFile `json:",omitempty"`
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *controlclient.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
@@ -94,67 +80,6 @@ type Notify struct {
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
|
||||
func (n Notify) String() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Notify{")
|
||||
if n.ErrMessage != nil {
|
||||
fmt.Fprintf(&sb, "err=%q ", *n.ErrMessage)
|
||||
}
|
||||
if n.LoginFinished != nil {
|
||||
sb.WriteString("LoginFinished ")
|
||||
}
|
||||
if n.State != nil {
|
||||
fmt.Fprintf(&sb, "state=%v ", *n.State)
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
sb.WriteString("NetMap{...} ")
|
||||
}
|
||||
if n.Engine != nil {
|
||||
fmt.Fprintf(&sb, "wg=%v ", *n.Engine)
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
sb.WriteString("URL=<...> ")
|
||||
}
|
||||
if n.BackendLogID != nil {
|
||||
sb.WriteString("BackendLogID ")
|
||||
}
|
||||
if n.PingResult != nil {
|
||||
fmt.Fprintf(&sb, "ping=%v ", *n.PingResult)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
sb.WriteString("FilesWaiting ")
|
||||
}
|
||||
if len(n.IncomingFiles) != 0 {
|
||||
sb.WriteString("IncomingFiles ")
|
||||
}
|
||||
if n.LocalTCPPort != nil {
|
||||
fmt.Fprintf(&sb, "tcpport=%v ", n.LocalTCPPort)
|
||||
}
|
||||
s := sb.String()
|
||||
return s[0:len(s)-1] + "}"
|
||||
}
|
||||
|
||||
// PartialFile represents an in-progress file transfer.
|
||||
type PartialFile struct {
|
||||
Name string // e.g. "foo.jpg"
|
||||
Started time.Time // time transfer started
|
||||
DeclaredSize int64 // or -1 if unknown
|
||||
Received int64 // bytes copied thus far
|
||||
|
||||
// PartialPath is set non-empty in "direct" file mode to the
|
||||
// in-progress '*.partial' file's path when the peerapi isn't
|
||||
// being used; see LocalBackend.SetDirectFileRoot.
|
||||
PartialPath string `json:",omitempty"`
|
||||
|
||||
// Done is set in "direct" mode when the partial file has been
|
||||
// closed and is ready for the caller to rename away the
|
||||
// ".partial" suffix.
|
||||
Done bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||
// (preferences, private keys, etc.).
|
||||
//
|
||||
@@ -185,33 +110,24 @@ type Options struct {
|
||||
// state and use/update that.
|
||||
// - StateKey!="" && Prefs!=nil: like the previous case, but do
|
||||
// an initial overwrite of backend state with Prefs.
|
||||
//
|
||||
// NOTE(apenwarr): The above means that this Prefs field does not do
|
||||
// what you probably think it does. It will overwrite your encryption
|
||||
// keys. Do not use unless you know what you're doing.
|
||||
StateKey StateKey
|
||||
Prefs *Prefs
|
||||
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
|
||||
// already stored in the backend state, *except* for the Persist
|
||||
// Persist member. If you just want to provide prefs, this is
|
||||
// probably what you want.
|
||||
//
|
||||
// UpdatePrefs.Persist is always ignored. Prefs.Persist will still
|
||||
// be used even if UpdatePrefs is provided. Other than Persist,
|
||||
// UpdatePrefs takes precedence over Prefs.
|
||||
//
|
||||
// This is intended as a purely temporary workaround for the
|
||||
// currently unexpected behaviour of Options.Prefs.
|
||||
//
|
||||
// TODO(apenwarr): Remove this, or rename Prefs to something else
|
||||
// and rename this to Prefs. Or, move Prefs.Persist elsewhere
|
||||
// entirely (as it always should have been), and then we wouldn't
|
||||
// need two separate fields at all. Or, move the fancy state
|
||||
// migration stuff out of Start().
|
||||
UpdatePrefs *Prefs
|
||||
// AuthKey is an optional node auth key used to authorize a
|
||||
// new node key without user interaction.
|
||||
AuthKey string
|
||||
// LegacyConfigPath optionally specifies the old-style relaynode
|
||||
// relay.conf location. If both LegacyConfigPath and StateKey are
|
||||
// specified and the requested state doesn't exist in the backend
|
||||
// store, the backend migrates the config from LegacyConfigPath.
|
||||
//
|
||||
// TODO(danderson): remove some time after the transition to
|
||||
// tailscaled is done.
|
||||
LegacyConfigPath string
|
||||
// Notify is called when backend events happen.
|
||||
Notify func(Notify) `json:"-"`
|
||||
// HTTPTestClient is an optional HTTP client to pass to controlclient
|
||||
// (for tests only).
|
||||
HTTPTestClient *http.Client
|
||||
}
|
||||
|
||||
// Backend is the interface between Tailscale frontends
|
||||
@@ -220,9 +136,6 @@ type Options struct {
|
||||
// (It has nothing to do with the interface between the backends
|
||||
// and the cloud control plane.)
|
||||
type Backend interface {
|
||||
// SetNotifyCallback sets the callback to be called on updates
|
||||
// from the backend to the client.
|
||||
SetNotifyCallback(func(Notify))
|
||||
// Start starts or restarts the backend, typically when a
|
||||
// frontend client connects.
|
||||
Start(Options) error
|
||||
@@ -231,7 +144,7 @@ type Backend interface {
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *tailcfg.Oauth2Token)
|
||||
Login(token *oauth2.Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
@@ -239,11 +152,17 @@ type Backend interface {
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*Prefs)
|
||||
// SetWantRunning is like SetPrefs but sets only the
|
||||
// WantRunning field.
|
||||
SetWantRunning(wantRunning bool)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
// polling.
|
||||
RequestEngineStatus()
|
||||
// RequestStatus requests that a full Status update
|
||||
// notification is sent.
|
||||
RequestStatus()
|
||||
// FakeExpireAfter pretends that the current key is going to
|
||||
// expire after duration x. This is useful for testing GUIs to
|
||||
// make sure they react properly with keys that are going to
|
||||
@@ -252,5 +171,5 @@ type Backend interface {
|
||||
// Ping attempts to start connecting to the given IP and sends a Notify
|
||||
// with its PingResult. If the host is down, there might never
|
||||
// be a PingResult sent. The cmd/tailscale CLI client adds a timeout.
|
||||
Ping(ip string, useTSMP bool)
|
||||
Ping(ip string)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type FakeBackend struct {
|
||||
@@ -19,29 +20,19 @@ type FakeBackend struct {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Start(opts Options) error {
|
||||
b.serverURL = opts.Prefs.ControlURLOrDefault()
|
||||
if b.notify == nil {
|
||||
panic("FakeBackend.Start: SetNotifyCallback not called")
|
||||
b.serverURL = opts.Prefs.ControlURL
|
||||
if opts.Notify == nil {
|
||||
log.Fatalf("FakeBackend.Start: opts.Notify is nil\n")
|
||||
}
|
||||
b.notify = opts.Notify
|
||||
b.notify(Notify{Prefs: opts.Prefs})
|
||||
nl := NeedsLogin
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: opts.Prefs})
|
||||
b.notify(Notify{State: &nl})
|
||||
}
|
||||
b.notify(Notify{State: &nl})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetNotifyCallback(notify func(Notify)) {
|
||||
if notify == nil {
|
||||
panic("FakeBackend.SetNotifyCallback: notify is nil")
|
||||
}
|
||||
b.notify = notify
|
||||
}
|
||||
|
||||
func (b *FakeBackend) newState(s State) {
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{State: &s})
|
||||
}
|
||||
b.notify(Notify{State: &s})
|
||||
if s == Running {
|
||||
b.live = true
|
||||
} else {
|
||||
@@ -51,13 +42,11 @@ func (b *FakeBackend) newState(s State) {
|
||||
|
||||
func (b *FakeBackend) StartLoginInteractive() {
|
||||
u := b.serverURL + "/this/is/fake"
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{BrowseToURL: &u})
|
||||
}
|
||||
b.notify(Notify{BrowseToURL: &u})
|
||||
b.login()
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Login(token *tailcfg.Oauth2Token) {
|
||||
func (b *FakeBackend) Login(token *oauth2.Token) {
|
||||
b.login()
|
||||
}
|
||||
|
||||
@@ -65,14 +54,10 @@ func (b *FakeBackend) login() {
|
||||
b.newState(NeedsMachineAuth)
|
||||
b.newState(Stopped)
|
||||
// TODO(apenwarr): Fill in a more interesting netmap here.
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
}
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.newState(Starting)
|
||||
// TODO(apenwarr): Fill in a more interesting status.
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
b.newState(Running)
|
||||
}
|
||||
|
||||
@@ -85,9 +70,7 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
panic("FakeBackend.SetPrefs got nil prefs")
|
||||
}
|
||||
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: new.Clone()})
|
||||
}
|
||||
b.notify(Notify{Prefs: new.Clone()})
|
||||
if new.WantRunning && !b.live {
|
||||
b.newState(Starting)
|
||||
b.newState(Running)
|
||||
@@ -96,20 +79,22 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetWantRunning(v bool) {
|
||||
b.SetPrefs(&Prefs{WantRunning: v})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestEngineStatus() {
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestStatus() {
|
||||
b.notify(Notify{Status: &ipnstate.Status{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
}
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string, useTSMP bool) {
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
|
||||
@@ -8,33 +8,32 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
b Backend
|
||||
logf logger.Logf
|
||||
frontendLogID string
|
||||
b Backend
|
||||
xnotify func(Notify)
|
||||
logf logger.Logf
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
xnotify func(Notify)
|
||||
frontendLogID string
|
||||
netmapCache *netmap.NetworkMap
|
||||
netmapCache *controlclient.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
}
|
||||
|
||||
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
|
||||
func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
|
||||
h := &Handle{
|
||||
b: b,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
h.SetNotifyCallback(notify)
|
||||
err := h.Start(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,25 +42,18 @@ func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
|
||||
h.mu.Lock()
|
||||
h.xnotify = notify
|
||||
h.mu.Unlock()
|
||||
|
||||
h.b.SetNotifyCallback(h.notify)
|
||||
}
|
||||
|
||||
func (h *Handle) Start(opts Options) error {
|
||||
h.mu.Lock()
|
||||
h.frontendLogID = opts.FrontendLogID
|
||||
h.xnotify = opts.Notify
|
||||
h.netmapCache = nil
|
||||
h.engineStatusCache = EngineStatus{}
|
||||
h.stateCache = NoState
|
||||
if opts.Prefs != nil {
|
||||
h.prefsCache = opts.Prefs.Clone()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
return h.b.Start(opts)
|
||||
xopts := opts
|
||||
xopts.Notify = h.notify
|
||||
return h.b.Start(xopts)
|
||||
}
|
||||
|
||||
func (h *Handle) Reset() {
|
||||
@@ -137,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
return []netaddr.IPPrefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *netmap.NetworkMap {
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -156,14 +148,14 @@ func (h *Handle) Expiry() time.Time {
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.prefsCache.ControlURLOrDefault() + "/admin/machines"
|
||||
return h.prefsCache.ControlURL + "/admin/machines"
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Login(token *tailcfg.Oauth2Token) {
|
||||
func (h *Handle) Login(token *oauth2.Token) {
|
||||
h.b.Login(token)
|
||||
}
|
||||
|
||||
@@ -175,6 +167,10 @@ func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestStatus() {
|
||||
h.b.RequestStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
||||
h.b.FakeExpireAfter(x)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,495 +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 ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNetworkMapCompare(t *testing.T) {
|
||||
prefix1, err := netaddr.ParseIPPrefix("192.168.0.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node1 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix1}}
|
||||
|
||||
prefix2, err := netaddr.ParseIPPrefix("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node2 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix2}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *netmap.NetworkMap
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"both nil",
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"b nil",
|
||||
&netmap.NetworkMap{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"a nil",
|
||||
nil,
|
||||
&netmap.NetworkMap{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"both default",
|
||||
&netmap.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names identical",
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names differ",
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map2"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Peers identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Peer list length",
|
||||
// length of Peers list differs
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node lists identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node lists differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := dnsMapsEqual(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inRemove(ip netaddr.IP) bool {
|
||||
for _, pfx := range removeFromDefaultRoute {
|
||||
if pfx.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestShrinkDefaultRoute(t *testing.T) {
|
||||
tests := []struct {
|
||||
route string
|
||||
in []string
|
||||
out []string
|
||||
localIPFn func(netaddr.IP) bool // true if this machine's local IP address should be "in" after shrinking.
|
||||
}{
|
||||
{
|
||||
route: "0.0.0.0/0",
|
||||
in: []string{"1.2.3.4", "25.0.0.1"},
|
||||
out: []string{
|
||||
"10.0.0.1",
|
||||
"10.255.255.255",
|
||||
"192.168.0.1",
|
||||
"192.168.255.255",
|
||||
"172.16.0.1",
|
||||
"172.31.255.255",
|
||||
"100.101.102.103",
|
||||
"224.0.0.1",
|
||||
"169.254.169.254",
|
||||
// Some random IPv6 stuff that shouldn't be in a v4
|
||||
// default route.
|
||||
"fe80::",
|
||||
"2601::1",
|
||||
},
|
||||
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is4() },
|
||||
},
|
||||
{
|
||||
route: "::/0",
|
||||
in: []string{"::1", "2601::1"},
|
||||
out: []string{
|
||||
"fe80::1",
|
||||
"ff00::1",
|
||||
tsaddr.TailscaleULARange().IP().String(),
|
||||
},
|
||||
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
def := netaddr.MustParseIPPrefix(test.route)
|
||||
got, err := shrinkDefaultRoute(def)
|
||||
if err != nil {
|
||||
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
|
||||
}
|
||||
for _, ip := range test.in {
|
||||
if !got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = false, want true", test.route, ip)
|
||||
}
|
||||
}
|
||||
for _, ip := range test.out {
|
||||
if got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
|
||||
}
|
||||
}
|
||||
ips, _, err := interfaces.LocalAddresses()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
want := test.localIPFn(ip)
|
||||
if gotContains := got.Contains(ip); gotContains != want {
|
||||
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRoutes(t *testing.T) {
|
||||
pp := netaddr.MustParseIPPrefix
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []wgcfg.Peer
|
||||
want []netaddr.IPPrefix
|
||||
}{
|
||||
{
|
||||
name: "small_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_1_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_2_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4_big_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerRoutes(tt.peers, 2)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeerAPIBase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
peer *tailcfg.Node
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil_netmap",
|
||||
peer: new(tailcfg.Node),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "nil_peer",
|
||||
nm: new(netmap.NetworkMap),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "self_only_4_them_both",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://100.64.1.2:444",
|
||||
},
|
||||
{
|
||||
name: "self_only_6_them_both",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://[fe70::2]:666",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_only_4",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://100.64.1.2:444",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_only_6",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://[fe70::2]:666",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_no_peerapi_service",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerAPIBase(tt.nm, tt.peer)
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type panicOnUseTransport struct{}
|
||||
|
||||
func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
panic("unexpected HTTP request")
|
||||
}
|
||||
|
||||
// Issue 1573: don't generate a machine key if we don't want to be running.
|
||||
func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
|
||||
panicOnMachineKeyGeneration = true
|
||||
|
||||
var logf logger.Logf = logger.Discard
|
||||
store := new(ipn.MemoryStore)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
lb, err := NewLocalBackend(logf, "logid", store, eng)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
lb.SetHTTPTestClient(&http.Client{
|
||||
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
|
||||
})
|
||||
|
||||
if err := lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
}); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
// Give the controlclient package goroutines (if they're
|
||||
// accidentally started) extra time to schedule and run (and thus
|
||||
// hit panicOnUseTransport).
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestFileTargets(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
_, err := b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
t.Errorf("before connect: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.netMap = new(netmap.NetworkMap)
|
||||
_, err = b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
t.Errorf("non-running netmap: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.state = ipn.Running
|
||||
_, err = b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "file sharing not enabled by Tailscale admin"; got != want {
|
||||
t.Errorf("without cap: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.capFileSharing = true
|
||||
got, err := b.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
// (other cases handled by TestPeerAPIBase above)
|
||||
}
|
||||
@@ -1,712 +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 ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
|
||||
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
selfNode *tailcfg.Node
|
||||
knownEmpty syncs.AtomicBool
|
||||
|
||||
// directFileMode is whether we're writing files directly to a
|
||||
// download directory (as *.partial files), rather than making
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on GUI macOS version.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
directFileMode bool
|
||||
}
|
||||
|
||||
const (
|
||||
// partialSuffix is the suffix appened to files while they're
|
||||
// still in the process of being transferred.
|
||||
partialSuffix = ".partial"
|
||||
|
||||
// deletedSuffix is the suffix for a deleted marker file
|
||||
// that's placed next to a file (without the suffix) that we
|
||||
// tried to delete, but Windows wouldn't let us. These are
|
||||
// only written on Windows (and in tests), but they're not
|
||||
// permitted to be uploaded directly on any platform, like
|
||||
// partial files.
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
return false
|
||||
case '\\', ':', '*', '"', '<', '>', '|':
|
||||
// Invalid stuff on Windows, but we reject them everywhere
|
||||
// for now.
|
||||
// TODO(bradfitz): figure out a better plan. We initially just
|
||||
// wrote things to disk URL path-escaped, but that's gross
|
||||
// when debugging, and just moves the problem to callers.
|
||||
// So now we put the UTF-8 filenames on disk directly as
|
||||
// sent.
|
||||
return false
|
||||
}
|
||||
return unicode.IsPrint(r)
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
|
||||
if !utf8.ValidString(baseName) {
|
||||
return "", false
|
||||
}
|
||||
if strings.TrimSpace(baseName) != baseName {
|
||||
return "", false
|
||||
}
|
||||
if len(baseName) > 255 {
|
||||
return "", false
|
||||
}
|
||||
// TODO: validate unicode normalization form too? Varies by platform.
|
||||
clean := path.Clean(baseName)
|
||||
if clean != baseName ||
|
||||
clean == "." || clean == ".." ||
|
||||
strings.HasSuffix(clean, deletedSuffix) ||
|
||||
strings.HasSuffix(clean, partialSuffix) {
|
||||
return "", false
|
||||
}
|
||||
for _, r := range baseName {
|
||||
if !validFilenameRune(r) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return filepath.Join(s.rootDir, baseName), true
|
||||
}
|
||||
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
if s.rootDir == "" || s.directFileMode {
|
||||
return false
|
||||
}
|
||||
if s.knownEmpty.Get() {
|
||||
// Optimization: this is usually empty, so avoid opening
|
||||
// the directory and checking. We can't cache the actual
|
||||
// has-files-or-not values as the macOS/iOS client might
|
||||
// in the future use+delete the files directly. So only
|
||||
// keep this negative cache.
|
||||
return false
|
||||
}
|
||||
f, err := os.Open(s.rootDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
// After we're done looping over files, then try
|
||||
// to delete this file. Don't do it proactively,
|
||||
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
|
||||
// and we don't want to delete the ".deleted" file before
|
||||
// enumerating to the "foo.jpg" file.
|
||||
defer tryDeleteAgain(filepath.Join(s.rootDir, strings.TrimSuffix(name, deletedSuffix)))
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
_, err := os.Stat(filepath.Join(s.rootDir, name+deletedSuffix))
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
tryDeleteAgain(filepath.Join(s.rootDir, name))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
s.knownEmpty.Set(true)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitingFiles returns the list of files that have been sent by a
|
||||
// peer that are waiting in the buffered "pick up" directory owned by
|
||||
// the Tailscale daemon.
|
||||
//
|
||||
// As a side effect, it also does any lazy deletion of files as
|
||||
// required by Windows.
|
||||
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, nil
|
||||
}
|
||||
f, err := os.Open(s.rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
if deleted == nil {
|
||||
deleted = map[string]bool{}
|
||||
}
|
||||
deleted[strings.TrimSuffix(name, deletedSuffix)] = true
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
fi, err := de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, apitype.WaitingFile{
|
||||
Name: filepath.Base(name),
|
||||
Size: fi.Size(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(deleted) > 0 {
|
||||
// Filter out any return values "foo.jpg" where a
|
||||
// "foo.jpg.deleted" marker file exists on disk.
|
||||
all := ret
|
||||
ret = ret[:0]
|
||||
for _, wf := range all {
|
||||
if !deleted[wf.Name] {
|
||||
ret = append(ret, wf)
|
||||
}
|
||||
}
|
||||
// And do some opportunistic deleting while we're here.
|
||||
// Maybe Windows is done virus scanning the file we tried
|
||||
// to delete a long time ago and will let us delete it now.
|
||||
for name := range deleted {
|
||||
tryDeleteAgain(filepath.Join(s.rootDir, name))
|
||||
}
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
|
||||
// it failed earlier. This happens on Windows when various anti-virus
|
||||
// tools hook into filesystem operations and have the file open still
|
||||
// while we're trying to delete it. In that case we instead mark it as
|
||||
// deleted (writing a "foo.jpg.deleted" marker file), but then we
|
||||
// later try to clean them up.
|
||||
//
|
||||
// fullPath is the full path to the file without the deleted suffix.
|
||||
func tryDeleteAgain(fullPath string) {
|
||||
if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
|
||||
os.Remove(fullPath + deletedSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
if s.directFileMode {
|
||||
return errors.New("deletes not allowed in direct mode")
|
||||
}
|
||||
path, ok := s.diskPath(baseName)
|
||||
if !ok {
|
||||
return errors.New("bad filename")
|
||||
}
|
||||
var bo *backoff.Backoff
|
||||
logf := s.b.logf
|
||||
t0 := time.Now()
|
||||
for {
|
||||
err := os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
err = redactErr(err)
|
||||
// Put a retry loop around deletes on Windows. Windows
|
||||
// file descriptor closes are effectively asynchronous,
|
||||
// as a bunch of hooks run on/after close, and we can't
|
||||
// necessarily delete the file for a while after close,
|
||||
// as we need to wait for everybody to be done with
|
||||
// it. (on Windows, unlike Unix, a file can't be deleted
|
||||
// if it's open anywhere)
|
||||
// So try a few times but ultimately just leave a
|
||||
// "foo.jpg.deleted" marker file to note that it's
|
||||
// deleted and we clean it up later.
|
||||
if runtime.GOOS == "windows" {
|
||||
if bo == nil {
|
||||
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
|
||||
}
|
||||
if time.Since(t0) < 5*time.Second {
|
||||
bo.BackOff(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err := touchFile(path + deletedSuffix); err != nil {
|
||||
logf("peerapi: failed to leave deleted marker: %v", err)
|
||||
}
|
||||
}
|
||||
logf("peerapi: failed to DeleteFile: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// redacted is a fake path name we use in errors, to avoid
|
||||
// accidentally logging actual filenames anywhere.
|
||||
const redacted = "redacted"
|
||||
|
||||
func redactErr(err error) error {
|
||||
if pe, ok := err.(*os.PathError); ok {
|
||||
pe.Path = redacted
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func touchFile(path string) error {
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return redactErr(err)
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if s.rootDir == "" {
|
||||
return nil, 0, errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, 0, errors.New("opens not allowed in direct mode")
|
||||
}
|
||||
path, ok := s.diskPath(baseName)
|
||||
if !ok {
|
||||
return nil, 0, errors.New("bad filename")
|
||||
}
|
||||
if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
|
||||
tryDeleteAgain(path)
|
||||
return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, redactErr(err)
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, 0, redactErr(err)
|
||||
}
|
||||
return f, fi.Size(), nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
|
||||
ipStr := ip.String()
|
||||
|
||||
var lc net.ListenConfig
|
||||
if initListenConfig != nil {
|
||||
// On iOS/macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to, to get
|
||||
// out of the network sandbox.
|
||||
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
ipStr = ""
|
||||
}
|
||||
}
|
||||
|
||||
if wgengine.IsNetstack(s.b.e) {
|
||||
ipStr = ""
|
||||
}
|
||||
|
||||
tcp4or6 := "tcp4"
|
||||
if ip.Is6() {
|
||||
tcp4or6 = "tcp6"
|
||||
}
|
||||
|
||||
// Make a best effort to pick a deterministic port number for
|
||||
// the ip The lower three bytes are the same for IPv4 and IPv6
|
||||
// Tailscale addresses (at least currently), so we'll usually
|
||||
// get the same port number on both address families for
|
||||
// dev/debugging purposes, which is nice. But it's not so
|
||||
// deterministic that people will bake this into clients.
|
||||
// We try a few times just in case something's already
|
||||
// listening on that port (on all interfaces, probably).
|
||||
for try := uint8(0); try < 5; try++ {
|
||||
a16 := ip.As16()
|
||||
hashData := a16[len(a16)-3:]
|
||||
hashData[0] += try
|
||||
tryPort := (32 << 10) | uint16(crc32.ChecksumIEEE(hashData))
|
||||
ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, strconv.Itoa(int(tryPort))))
|
||||
if err == nil {
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
// Fall back to random ephemeral port.
|
||||
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
|
||||
}
|
||||
|
||||
type peerAPIListener struct {
|
||||
ps *peerAPIServer
|
||||
ip netaddr.IP
|
||||
lb *LocalBackend
|
||||
|
||||
// ln is the Listener. It can be nil in netstack mode if there are more than
|
||||
// 1 local addresses (e.g. both an IPv4 and IPv6). When it's nil, port
|
||||
// and urlStr are still populated.
|
||||
ln net.Listener
|
||||
|
||||
// urlStr is the base URL to access the peer API (http://ip:port/).
|
||||
urlStr string
|
||||
// port is just the port of urlStr.
|
||||
port int
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) Close() error {
|
||||
if pln.ln != nil {
|
||||
return pln.ln.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) serve() {
|
||||
if pln.ln == nil {
|
||||
return
|
||||
}
|
||||
defer pln.ln.Close()
|
||||
logf := pln.lb.logf
|
||||
for {
|
||||
c, err := pln.ln.Accept()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logf("peerapi.Accept: %v", err)
|
||||
return
|
||||
}
|
||||
ta, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
c.Close()
|
||||
logf("peerapi: unexpected RemoteAddr %#v", c.RemoteAddr())
|
||||
continue
|
||||
}
|
||||
ipp, ok := netaddr.FromStdAddr(ta.IP, ta.Port, "")
|
||||
if !ok {
|
||||
logf("peerapi: bogus TCPAddr %#v", ta)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", ipp)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
}
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
net.Listener
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
// peerAPIHandler serves the Peer API for a source specific client.
|
||||
type peerAPIHandler struct {
|
||||
ps *peerAPIServer
|
||||
remoteAddr netaddr.IPPort
|
||||
isSelf bool // whether peerNode is owned by same user as this node
|
||||
peerNode *tailcfg.Node // peerNode is who's making the request
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) logf(format string, a ...interface{}) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v0/goroutines" {
|
||||
h.handleServeGoroutines(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<body>
|
||||
<h1>Hello, %s (%v)</h1>
|
||||
This is my Tailscale device. Your device is %v.
|
||||
`, html.EscapeString(who), h.remoteAddr.IP(), html.EscapeString(h.peerNode.ComputedName))
|
||||
|
||||
if h.isSelf {
|
||||
fmt.Fprintf(w, "<p>You are the owner of this node.\n")
|
||||
}
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
ph *peerAPIHandler
|
||||
partialPath string // non-empty in direct mode
|
||||
|
||||
mu sync.Mutex
|
||||
copied int64
|
||||
done bool
|
||||
lastNotify time.Time
|
||||
}
|
||||
|
||||
func (f *incomingFile) markAndNotifyDone() {
|
||||
f.mu.Lock()
|
||||
f.done = true
|
||||
f.mu.Unlock()
|
||||
b := f.ph.ps.b
|
||||
b.sendFileNotify()
|
||||
}
|
||||
|
||||
func (f *incomingFile) Write(p []byte) (n int, err error) {
|
||||
n, err = f.w.Write(p)
|
||||
|
||||
b := f.ph.ps.b
|
||||
var needNotify bool
|
||||
defer func() {
|
||||
if needNotify {
|
||||
b.sendFileNotify()
|
||||
}
|
||||
}()
|
||||
if n > 0 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.copied += int64(n)
|
||||
now := time.Now()
|
||||
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
|
||||
f.lastNotify = now
|
||||
needNotify = true
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return ipn.PartialFile{
|
||||
Name: f.name,
|
||||
Started: f.started,
|
||||
DeclaredSize: f.size,
|
||||
Received: f.copied,
|
||||
PartialPath: f.partialPath,
|
||||
Done: f.done,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.ps.b.hasCapFileSharing() {
|
||||
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
|
||||
if suffix == rawPath {
|
||||
http.Error(w, "misconfigured internals", 500)
|
||||
return
|
||||
}
|
||||
if suffix == "" {
|
||||
http.Error(w, "empty filename", 400)
|
||||
return
|
||||
}
|
||||
if strings.Contains(suffix, "/") {
|
||||
http.Error(w, "directories not supported", 400)
|
||||
return
|
||||
}
|
||||
baseName, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, "bad path encoding", 400)
|
||||
return
|
||||
}
|
||||
dstFile, ok := h.ps.diskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
t0 := time.Now()
|
||||
// TODO(bradfitz): prevent same filename being sent by two peers at once
|
||||
partialFile := dstFile + partialSuffix
|
||||
f, err := os.Create(partialFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", redactErr(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
os.Remove(partialFile)
|
||||
}
|
||||
}()
|
||||
var finalSize int64
|
||||
var inFile *incomingFile
|
||||
if r.ContentLength != 0 {
|
||||
inFile = &incomingFile{
|
||||
name: baseName,
|
||||
started: time.Now(),
|
||||
size: r.ContentLength,
|
||||
w: f,
|
||||
ph: h,
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
inFile.partialPath = partialFile
|
||||
}
|
||||
h.ps.b.registerIncomingFile(inFile, true)
|
||||
defer h.ps.b.registerIncomingFile(inFile, false)
|
||||
n, err := io.Copy(inFile, r.Body)
|
||||
if err != nil {
|
||||
err = redactErr(err)
|
||||
f.Close()
|
||||
h.logf("put Copy error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
finalSize = n
|
||||
}
|
||||
if err := redactErr(f.Close()); err != nil {
|
||||
h.logf("put Close error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(partialFile, dstFile); err != nil {
|
||||
err = redactErr(err)
|
||||
h.logf("put final rename: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d := time.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
success = true
|
||||
io.WriteString(w, "{}\n")
|
||||
h.ps.knownEmpty.Set(false)
|
||||
h.ps.b.sendFileNotify()
|
||||
}
|
||||
|
||||
func approxSize(n int64) string {
|
||||
if n <= 1<<10 {
|
||||
return "<=1KB"
|
||||
}
|
||||
if n <= 1<<20 {
|
||||
return "<=1MB"
|
||||
}
|
||||
return fmt.Sprintf("~%dMB", n>>20)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var buf []byte
|
||||
for size := 4 << 10; size <= 2<<20; size *= 2 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
w.Write(buf)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user