Compare commits
2 Commits
walterp-ap
...
bradfitz/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
698defd54b | ||
|
|
c378a9900c |
17
.github/licenses.tmpl
vendored
17
.github/licenses.tmpl
vendored
@@ -1,17 +0,0 @@
|
||||
# Tailscale CLI and daemon dependencies
|
||||
|
||||
The following open source dependencies are used to build the [tailscale][] and
|
||||
[tailscaled][] commands. These are primarily used on Linux and BSD variants as
|
||||
well as an [option for macOS][].
|
||||
|
||||
[tailscale]: https://pkg.go.dev/tailscale.com/cmd/tailscale
|
||||
[tailscaled]: https://pkg.go.dev/tailscale.com/cmd/tailscaled
|
||||
[option for macOS]: https://tailscale.com/kb/1065/macos-variants/
|
||||
|
||||
## Go Packages
|
||||
|
||||
Some packages may only be included on certain architectures or operating systems.
|
||||
|
||||
{{ range . }}
|
||||
- [{{.Name}}](https://pkg.go.dev/{{.Name}}) ([{{.LicenseName}}]({{.LicenseURL}}))
|
||||
{{- end }}
|
||||
5
.github/workflows/cifuzz.yml
vendored
5
.github/workflows/cifuzz.yml
vendored
@@ -1,10 +1,5 @@
|
||||
name: CIFuzz
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -20,10 +20,6 @@ on:
|
||||
schedule:
|
||||
- cron: '31 14 * * 5'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
||||
11
.github/workflows/cross-darwin.yml
vendored
11
.github/workflows/cross-darwin.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: macOS build cmd
|
||||
env:
|
||||
GOOS: darwin
|
||||
|
||||
11
.github/workflows/cross-freebsd.yml
vendored
11
.github/workflows/cross-freebsd.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: FreeBSD build cmd
|
||||
env:
|
||||
GOOS: freebsd
|
||||
|
||||
11
.github/workflows/cross-openbsd.yml
vendored
11
.github/workflows/cross-openbsd.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: OpenBSD build cmd
|
||||
env:
|
||||
GOOS: openbsd
|
||||
|
||||
15
.github/workflows/cross-wasm.yml
vendored
15
.github/workflows/cross-wasm.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Wasm client build
|
||||
env:
|
||||
GOOS: js
|
||||
@@ -37,9 +34,7 @@ jobs:
|
||||
- name: tsconnect static build
|
||||
# Use our custom Go toolchain, we set build tags (to control binary size)
|
||||
# that depend on it.
|
||||
run: |
|
||||
./tool/go run ./cmd/tsconnect --fast-compression build
|
||||
./tool/go run ./cmd/tsconnect build-pkg
|
||||
run: ./tool/go run ./cmd/tsconnect --fast-compression build
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
11
.github/workflows/cross-windows.yml
vendored
11
.github/workflows/cross-windows.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Windows build cmd
|
||||
env:
|
||||
GOOS: windows
|
||||
|
||||
22
.github/workflows/depaware.yml
vendored
22
.github/workflows/depaware.yml
vendored
@@ -8,25 +8,21 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
|
||||
- name: depaware
|
||||
run: go run github.com/tailscale/depaware --check
|
||||
tailscale.com/cmd/tailscaled
|
||||
tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/derper
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: depaware tailscaled
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
|
||||
- name: depaware tailscale
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
64
.github/workflows/go-licenses.yml
vendored
64
.github/workflows/go-licenses.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: go-licenses
|
||||
|
||||
on:
|
||||
# run action when a change lands in the main branch which updates go.mod or
|
||||
# our license template file. Also allow manual triggering.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- go.mod
|
||||
- .github/licenses.tmpl
|
||||
- .github/workflows/go-licenses.yml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tailscale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install go-licenses
|
||||
run: |
|
||||
go install github.com/google/go-licenses@v1.2.2-0.20220825154955-5eedde1c6584
|
||||
|
||||
- name: Run go-licenses
|
||||
env:
|
||||
# include all build tags to include platform-specific dependencies
|
||||
GOFLAGS: "-tags=android,cgo,darwin,freebsd,ios,js,linux,openbsd,wasm,windows"
|
||||
run: |
|
||||
[ -d licenses ] || mkdir licenses
|
||||
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@18f90432bedd2afd6a825469ffd38aa24712a91d #v4.1.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply@tailscale.com>
|
||||
committer: License Updater <noreply@tailscale.com>
|
||||
branch: licenses/cli
|
||||
commit-message: "licenses: update tailscale{,d} licenses"
|
||||
title: "licenses: update tailscale{,d} licenses"
|
||||
body: Triggered by ${{ github.repository }}@${{ github.sha }}
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
team-reviewers: opensource-license-reviewers
|
||||
14
.github/workflows/go_generate.yml
vendored
14
.github/workflows/go_generate.yml
vendored
@@ -9,25 +9,21 @@ on:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
if [[ "${{github.ref}}" == release-branch/* ]]
|
||||
|
||||
35
.github/workflows/go_mod_tidy.yml
vendored
35
.github/workflows/go_mod_tidy.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: go mod tidy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: check 'go mod tidy' is clean
|
||||
run: |
|
||||
go mod tidy
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "Please run 'go mod tidy'."; exit 1)
|
||||
12
.github/workflows/license.yml
vendored
12
.github/workflows/license.yml
vendored
@@ -8,22 +8,18 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run license checker
|
||||
run: ./scripts/check_license_headers.sh .
|
||||
|
||||
11
.github/workflows/linux-race.yml
vendored
11
.github/workflows/linux-race.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
|
||||
11
.github/workflows/linux.yml
vendored
11
.github/workflows/linux.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
|
||||
11
.github/workflows/linux32.yml
vendored
11
.github/workflows/linux32.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,15 +15,16 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Basic build
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
|
||||
|
||||
112
.github/workflows/static-analysis.yml
vendored
112
.github/workflows/static-analysis.yml
vendored
@@ -1,112 +0,0 @@
|
||||
name: static-analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
gofmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run gofmt (goimports)
|
||||
run: go run golang.org/x/tools/cmd/goimports -d --format-only .
|
||||
- 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'
|
||||
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
- 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'
|
||||
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: [amd64]
|
||||
include:
|
||||
- goos: windows
|
||||
goarch: 386
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install staticcheck
|
||||
run: "GOBIN=~/.local/bin go install honnef.co/go/tools/cmd/staticcheck"
|
||||
|
||||
- name: Print staticcheck version
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: "Run staticcheck (${{ matrix.goos }}/${{ matrix.goarch }})"
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- 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'
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Android-Cross
|
||||
name: staticcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,35 +8,54 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
go-version: 1.19
|
||||
|
||||
- name: Android smoke build
|
||||
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
|
||||
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
|
||||
# some Android breakages early.
|
||||
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run gofmt (goimports)
|
||||
run: go run golang.org/x/tools/cmd/goimports -d --format-only .
|
||||
|
||||
- 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"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
env:
|
||||
GOOS: android
|
||||
GOARCH: arm64
|
||||
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: "386"
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
30
.github/workflows/tsconnect-pkg-publish.yml
vendored
30
.github/workflows/tsconnect-pkg-publish.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: "@tailscale/connect npm publish"
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Build package
|
||||
# Build with build_dist.sh to ensure that version information is embedded.
|
||||
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
|
||||
# also picks up our custom Go toolchain.
|
||||
run: |
|
||||
./build_dist.sh tailscale.com/cmd/tsconnect
|
||||
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
|
||||
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public
|
||||
12
.github/workflows/vm.yml
vendored
12
.github/workflows/vm.yml
vendored
@@ -5,10 +5,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ubuntu2004-LTS-cloud-base:
|
||||
runs-on: [ self-hosted, linux, vm ]
|
||||
@@ -19,13 +15,13 @@ jobs:
|
||||
- name: Set GOPATH
|
||||
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
|
||||
|
||||
77
.github/workflows/windows-race.yml
vendored
Normal file
77
.github/workflows/windows-race.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
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@v3
|
||||
with:
|
||||
go-version: 1.19.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# Note: unlike some other setups, this is only grabbing the mod download
|
||||
# cache, rather than the whole mod directory, as the download cache
|
||||
# contains zips that can be unpacked in parallel faster than they can be
|
||||
# fetched and extracted by tar
|
||||
path: |
|
||||
~/go/pkg/mod/cache
|
||||
~\AppData\Local\go-build
|
||||
|
||||
# The -2- here should be incremented when the scheme of data to be
|
||||
# cached changes (e.g. path above changes).
|
||||
# The -race- here ensures that non-race builds and race builds do not
|
||||
# overwrite each others cache, as while they share some files, they
|
||||
# differ in most by volume (build cache).
|
||||
# TODO(raggi): add a go version here.
|
||||
key: ${{ runner.os }}-go-2-race-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Print toolchain details
|
||||
run: gcc -v
|
||||
|
||||
# There is currently an issue in the race detector in Go on Windows when
|
||||
# used with a newer version of GCC.
|
||||
# See https://github.com/tailscale/tailscale/issues/4926.
|
||||
- name: Downgrade MinGW
|
||||
shell: bash
|
||||
run: |
|
||||
choco install mingw --version 10.2.0 --allow-downgrade
|
||||
|
||||
- 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'
|
||||
|
||||
11
.github/workflows/windows.yml
vendored
11
.github/workflows/windows.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
@@ -19,13 +15,14 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: 1.19.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.19-alpine AS build-env
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
|
||||
17
Makefile
17
Makefile
@@ -12,16 +12,12 @@ tidy:
|
||||
./tool/go mod tidy
|
||||
|
||||
updatedeps:
|
||||
./tool/go run github.com/tailscale/depaware --update \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper
|
||||
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
|
||||
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
|
||||
|
||||
depaware:
|
||||
./tool/go run github.com/tailscale/depaware --check \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper
|
||||
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
buildwindows:
|
||||
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
@@ -32,13 +28,10 @@ build386:
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
buildwasm:
|
||||
GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
|
||||
|
||||
buildmultiarchimage:
|
||||
./build_docker.sh
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.31.0
|
||||
1.29.0
|
||||
|
||||
132
api.md
132
api.md
@@ -3,12 +3,11 @@
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
|
||||
|
||||
# APIs
|
||||
|
||||
- **[Devices](#device)**
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
@@ -20,12 +19,12 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST device tags](#device-tags-post)
|
||||
- Key
|
||||
- [POST device key](#device-key-post)
|
||||
- **[Tailnets](#tailnet)**
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post)
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post)
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [Keys](#tailnet-keys)
|
||||
@@ -42,9 +41,7 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
|
||||
<!-- TODO: description about what devices are -->
|
||||
|
||||
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
|
||||
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
|
||||
|
||||
@@ -55,23 +52,20 @@ This is your deviceID.
|
||||
<a name=device-get></a>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
|
||||
- `all`: returns all fields in the response.
|
||||
- `default`: return all fields except:
|
||||
- `enabledRoutes`
|
||||
- `advertisedRoutes`
|
||||
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
* `all`: returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
@@ -79,7 +73,6 @@ For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/device/12345
|
||||
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
@@ -87,7 +80,6 @@ curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
@@ -149,18 +141,16 @@ Response
|
||||
<a name=device-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
|
||||
Deletes the provided device from its tailnet.
|
||||
The device must belong to the user's tailnet.
|
||||
Deleting shared/external devices is not supported.
|
||||
Supply the device of interest in the path using its ID.
|
||||
|
||||
##### Parameters
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
DELETE /api/v2/device/12345
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
@@ -170,7 +160,6 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
Response
|
||||
|
||||
If successful, the response should be empty:
|
||||
|
||||
```
|
||||
< HTTP/1.1 200 OK
|
||||
...
|
||||
@@ -179,13 +168,13 @@ If successful, the response should be empty:
|
||||
```
|
||||
|
||||
If the device is not owned by your tailnet:
|
||||
|
||||
```
|
||||
< HTTP/1.1 501 Not Implemented
|
||||
...
|
||||
{"message":"cannot delete devices outside of your tailnet"}
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></a>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
@@ -204,7 +193,6 @@ curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
@@ -225,9 +213,7 @@ Sets which subnet routes are enabled to be routed by a device by replacing the e
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`routes` - The new list of enabled subnet routes in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
|
||||
@@ -268,9 +254,7 @@ Marks a device as authorized, for Tailnets where device authorization is require
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`authorized` - whether the device is authorized; only `true` is currently supported.
|
||||
|
||||
```
|
||||
{
|
||||
"authorized": true
|
||||
@@ -351,9 +335,9 @@ The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
## Tailnet
|
||||
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the [Settings](https://login.tailscale.com/admin/settings/) page of the admin console.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
|
||||
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
@@ -362,10 +346,10 @@ GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
```
|
||||
|
||||
|
||||
For solo plans, the tailnet is the email you signed up with.
|
||||
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
|
||||
Her API calls would have the following format:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
@@ -386,19 +370,15 @@ Retrieves the ACL that is currently set for the given tailnet. Supply the tailne
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
|
||||
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
|
||||
|
||||
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
|
||||
|
||||
##### Example
|
||||
|
||||
###### Requesting a HuJSON response:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -408,7 +388,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
@@ -447,7 +426,6 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -457,7 +435,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
@@ -500,7 +477,6 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
@@ -510,16 +486,15 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
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.
|
||||
* `Groups` - Static groups of users which can be used for ACL rules.
|
||||
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
* `ACLs` - Access control lists.
|
||||
* `TagOwners` - Defines who is allowed to use which tags.
|
||||
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on those properties.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -549,7 +524,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
@@ -575,7 +549,6 @@ Response:
|
||||
```
|
||||
|
||||
Failed test error response:
|
||||
|
||||
```
|
||||
{
|
||||
"message": "test(s) failed",
|
||||
@@ -599,17 +572,14 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFor=user1@example.com&type=user' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
@@ -637,7 +607,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFo
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
@@ -662,7 +631,6 @@ The POST body should be a JSON formatted array of ACL Tests.
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example with tests
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
@@ -674,7 +642,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
```
|
||||
|
||||
##### Example with an ACL body
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
@@ -717,23 +684,21 @@ An empty body or a JSON object with no `message` is returned on success.
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
|
||||
- `all`: Returns all fields in the response.
|
||||
- `default`: return all fields except:
|
||||
- `enabledRoutes`
|
||||
- `advertisedRoutes`
|
||||
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
* `all`: Returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
@@ -749,7 +714,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
@@ -812,7 +776,6 @@ for the user who owns the API key used to perform this query.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
@@ -829,7 +792,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{"keys": [
|
||||
{"id": "kYKVU14CNTRL"},
|
||||
@@ -850,9 +812,7 @@ Supply the tailnet in the path.
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
@@ -899,7 +859,6 @@ echo '{
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
@@ -918,7 +877,6 @@ Returns a JSON object with information about specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
@@ -935,7 +893,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
@@ -965,11 +922,9 @@ Deletes a specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
This reports status 200 upon success.
|
||||
|
||||
##### Example
|
||||
@@ -986,12 +941,10 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k12345
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
@@ -1003,7 +956,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
@@ -1013,17 +965,13 @@ Response
|
||||
<a name=tailnet-dns-nameservers-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
|
||||
|
||||
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
|
||||
Supply the tailnet of interest in the path.
|
||||
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`dns` - The new list of DNS nameservers in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"]
|
||||
@@ -1031,15 +979,12 @@ Note that changing the list of DNS nameservers may also affect the status of Mag
|
||||
```
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns the new list of nameservers and the status of MagicDNS.
|
||||
|
||||
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
|
||||
|
||||
##### Example
|
||||
|
||||
###### Adding DNS nameservers with the MagicDNS on:
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
@@ -1048,7 +993,6 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
@@ -1057,7 +1001,6 @@ Response:
|
||||
```
|
||||
|
||||
###### Removing all DNS nameservers with the MagicDNS on:
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
@@ -1066,7 +1009,6 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
@@ -1077,16 +1019,13 @@ Response:
|
||||
<a name=tailnet-dns-preferences-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
|
||||
|
||||
Retrieves the DNS preferences that are currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
@@ -1094,7 +1033,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
@@ -1104,7 +1042,6 @@ Response:
|
||||
<a name=tailnet-dns-preferences-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
|
||||
|
||||
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
|
||||
@@ -1114,12 +1051,9 @@ Note that removing all nameservers will turn off MagicDNS.
|
||||
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
@@ -1127,7 +1061,6 @@ The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
@@ -1135,10 +1068,10 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferenc
|
||||
--data-binary '{"magicDNS": true}'
|
||||
```
|
||||
|
||||
|
||||
Response:
|
||||
|
||||
If there are no DNS servers, it returns an error message:
|
||||
|
||||
```
|
||||
{
|
||||
"message":"need at least one nameserver to enable MagicDNS"
|
||||
@@ -1146,7 +1079,6 @@ If there are no DNS servers, it returns an error message:
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
@@ -1156,16 +1088,14 @@ If there are DNS servers:
|
||||
<a name=tailnet-dns-searchpaths-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
|
||||
|
||||
Retrieves the list of search paths that is currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
@@ -1173,7 +1103,6 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
@@ -1183,15 +1112,12 @@ Response:
|
||||
<a name=tailnet-dns-searchpaths-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
|
||||
|
||||
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`searchPaths` - A list of searchpaths in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"]
|
||||
@@ -1199,7 +1125,6 @@ Replaces the list of searchpaths with the list supplied by the user and returns
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
@@ -1208,7 +1133,6 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpat
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
package atomicfile // import "tailscale.com/atomicfile"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename. The perm argument is ignored on Windows.
|
||||
// into filename.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,31 +11,15 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Maximum amount of time we should wait when reading a response from BIRD.
|
||||
responseTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// New creates a BIRDClient.
|
||||
func New(socket string) (*BIRDClient, error) {
|
||||
return newWithTimeout(socket, responseTimeout)
|
||||
}
|
||||
|
||||
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
b := &BIRDClient{
|
||||
socket: socket,
|
||||
conn: conn,
|
||||
scanner: bufio.NewScanner(conn),
|
||||
timeNow: time.Now,
|
||||
timeout: timeout,
|
||||
}
|
||||
b := &BIRDClient{socket: socket, conn: conn, scanner: bufio.NewScanner(conn)}
|
||||
// Read and discard the first line as that is the welcome message.
|
||||
if _, err := b.readResponse(); err != nil {
|
||||
return nil, err
|
||||
@@ -48,8 +32,6 @@ type BIRDClient struct {
|
||||
socket string
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
timeNow func() time.Time
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// Close closes the underlying connection to BIRD.
|
||||
@@ -99,15 +81,10 @@ func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
// 1 means ‘table entry’, 8 ‘runtime error’ and 9 ‘syntax error’.
|
||||
|
||||
func (b *BIRDClient) exec(cmd string, args ...any) (string, error) {
|
||||
if err := b.conn.SetWriteDeadline(b.timeNow().Add(b.timeout)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := fmt.Fprintln(b.conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintln(b.conn)
|
||||
return b.readResponse()
|
||||
}
|
||||
|
||||
@@ -128,20 +105,14 @@ func hasResponseCode(s []byte) bool {
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readResponse() (string, error) {
|
||||
// Set the read timeout before we start reading anything.
|
||||
if err := b.conn.SetReadDeadline(b.timeNow().Add(b.timeout)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var resp strings.Builder
|
||||
var done bool
|
||||
for !done {
|
||||
if !b.scanner.Scan() {
|
||||
if err := b.scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("reading response from bird failed (EOF): %q", resp.String())
|
||||
return "", fmt.Errorf("reading response from bird failed: %q", resp.String())
|
||||
}
|
||||
if err := b.scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
out := b.scanner.Bytes()
|
||||
if _, err := resp.Write(out); err != nil {
|
||||
|
||||
@@ -8,12 +8,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeBIRD struct {
|
||||
@@ -112,82 +109,3 @@ func TestChirp(t *testing.T) {
|
||||
t.Fatalf("disabling %q succeded", "rando")
|
||||
}
|
||||
}
|
||||
|
||||
type hangingListener struct {
|
||||
net.Listener
|
||||
t *testing.T
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
sock string
|
||||
}
|
||||
|
||||
func newHangingListener(t *testing.T) *hangingListener {
|
||||
sock := filepath.Join(t.TempDir(), "sock")
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &hangingListener{
|
||||
Listener: l,
|
||||
t: t,
|
||||
done: make(chan struct{}),
|
||||
sock: sock,
|
||||
}
|
||||
}
|
||||
|
||||
func (hl *hangingListener) Stop() {
|
||||
hl.Close()
|
||||
close(hl.done)
|
||||
hl.wg.Wait()
|
||||
}
|
||||
|
||||
func (hl *hangingListener) listen() error {
|
||||
for {
|
||||
c, err := hl.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
hl.wg.Add(1)
|
||||
go hl.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (hl *hangingListener) handle(c net.Conn) {
|
||||
defer hl.wg.Done()
|
||||
|
||||
// Write our fake first line of response so that we get into the read loop
|
||||
fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.")
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
hl.t.Logf("connection still hanging")
|
||||
case <-hl.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChirpTimeout(t *testing.T) {
|
||||
fb := newHangingListener(t)
|
||||
defer fb.Stop()
|
||||
go fb.listen()
|
||||
|
||||
c, err := newWithTimeout(fb.sock, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.EnableProtocol("tailscale")
|
||||
if err == nil {
|
||||
t.Fatal("got err=nil, want timeout")
|
||||
}
|
||||
if !os.IsTimeout(err) {
|
||||
t.Fatalf("got err=%v, want os.IsTimeout(err)=true", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -136,7 +136,7 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
|
||||
onVersionMismatch(ipn.IPCVersion(), server)
|
||||
}
|
||||
if res.StatusCode == 403 {
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
all, _ := ioutil.ReadAll(res.Body)
|
||||
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
|
||||
}
|
||||
return res, nil
|
||||
@@ -206,7 +206,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, err := io.ReadAll(res.Body)
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -364,7 +364,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
|
||||
return nil, 0, fmt.Errorf("unexpected chunking")
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
@@ -680,42 +680,6 @@ func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||
func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
pr := new(ipnstate.NetworkLockStatus)
|
||||
if err := json.Unmarshal(body, pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
|
||||
pr := new(ipnstate.NetworkLockStatus)
|
||||
if err := json.Unmarshal(body, pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
||||
// platform equivalent) is not answering localapi connections.
|
||||
//
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -130,7 +131,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
|
||||
|
||||
// Read response. Limit the response to 10MB.
|
||||
body := io.LimitReader(resp.Body, maxReadSize+1)
|
||||
b, err := io.ReadAll(body)
|
||||
b, err := ioutil.ReadAll(body)
|
||||
if len(b) > maxReadSize {
|
||||
err = errors.New("API response too large")
|
||||
}
|
||||
|
||||
@@ -12,36 +12,20 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
const refreshTimeout = time.Minute
|
||||
var dnsCache atomic.Value // of []byte
|
||||
|
||||
type dnsEntryMap map[string][]net.IP
|
||||
|
||||
var (
|
||||
dnsCache syncs.AtomicValue[dnsEntryMap]
|
||||
dnsCacheBytes syncs.AtomicValue[[]byte] // of JSON
|
||||
unpublishedDNSCache syncs.AtomicValue[dnsEntryMap]
|
||||
)
|
||||
|
||||
var (
|
||||
bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
publishedDNSHits = expvar.NewInt("counter_bootstrap_dns_published_hits")
|
||||
publishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_published_misses")
|
||||
unpublishedDNSHits = expvar.NewInt("counter_bootstrap_dns_unpublished_hits")
|
||||
unpublishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_misses")
|
||||
)
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
func refreshBootstrapDNSLoop() {
|
||||
if *bootstrapDNS == "" && *unpublishedDNS == "" {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
for {
|
||||
refreshBootstrapDNS()
|
||||
refreshUnpublishedDNS()
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
||||
@@ -50,34 +34,10 @@ func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
dnsEntries := make(map[string][]net.IP)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
return
|
||||
}
|
||||
|
||||
dnsCache.Store(dnsEntries)
|
||||
dnsCacheBytes.Store(j)
|
||||
}
|
||||
|
||||
func refreshUnpublishedDNS() {
|
||||
if *unpublishedDNS == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
defer cancel()
|
||||
|
||||
dnsEntries := resolveList(ctx, strings.Split(*unpublishedDNS, ","))
|
||||
unpublishedDNSCache.Store(dnsEntries)
|
||||
}
|
||||
|
||||
func resolveList(ctx context.Context, names []string) dnsEntryMap {
|
||||
dnsEntries := make(dnsEntryMap)
|
||||
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
var r net.Resolver
|
||||
for _, name := range names {
|
||||
addrs, err := r.LookupIP(ctx, "ip", name)
|
||||
@@ -87,47 +47,21 @@ func resolveList(ctx context.Context, names []string) dnsEntryMap {
|
||||
}
|
||||
dnsEntries[name] = addrs
|
||||
}
|
||||
return dnsEntries
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
return
|
||||
}
|
||||
dnsCache.Store(j)
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Bootstrap DNS requests occur cross-regions, and are randomized per
|
||||
// request, so keeping a connection open is pointlessly expensive.
|
||||
j, _ := dnsCache.Load().([]byte)
|
||||
// Bootstrap DNS requests occur cross-regions,
|
||||
// and are randomized per request,
|
||||
// so keeping a connection open is pointlessly expensive.
|
||||
w.Header().Set("Connection", "close")
|
||||
|
||||
// Try answering a query from our hidden map first
|
||||
if q := r.URL.Query().Get("q"); q != "" {
|
||||
if ips, ok := unpublishedDNSCache.Load()[q]; ok && len(ips) > 0 {
|
||||
unpublishedDNSHits.Add(1)
|
||||
|
||||
// Only return the specific query, not everything.
|
||||
m := dnsEntryMap{q: ips}
|
||||
j, err := json.MarshalIndent(m, "", "\t")
|
||||
if err == nil {
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a "q" query for a name in the published cache
|
||||
// list, then track whether that's a hit/miss.
|
||||
if m, ok := dnsCache.Load()[q]; ok {
|
||||
if len(m) > 0 {
|
||||
publishedDNSHits.Add(1)
|
||||
} else {
|
||||
publishedDNSMisses.Add(1)
|
||||
}
|
||||
} else {
|
||||
// If it wasn't in either cache, treat this as a query
|
||||
// for the unpublished cache, and thus a cache miss.
|
||||
unpublishedDNSMisses.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to returning the public set of cached DNS names
|
||||
j := dnsCacheBytes.Load()
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -22,12 +17,11 @@ func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
}()
|
||||
refreshBootstrapDNS()
|
||||
w := new(bitbucketResponseWriter)
|
||||
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(b *testing.PB) {
|
||||
for b.Next() {
|
||||
handleBootstrapDNS(w, req)
|
||||
handleBootstrapDNS(w, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -39,116 +33,3 @@ func (b *bitbucketResponseWriter) Header() http.Header { return make(http.Header
|
||||
func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}
|
||||
|
||||
func getBootstrapDNS(t *testing.T, q string) dnsEntryMap {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape(q), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handleBootstrapDNS(w, req)
|
||||
|
||||
res := w.Result()
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("got status=%d; want %d", res.StatusCode, 200)
|
||||
}
|
||||
var ips dnsEntryMap
|
||||
if err := json.NewDecoder(res.Body).Decode(&ips); err != nil {
|
||||
t.Fatalf("error decoding response body: %v", err)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func TestUnpublishedDNS(t *testing.T) {
|
||||
const published = "login.tailscale.com"
|
||||
const unpublished = "log.tailscale.io"
|
||||
|
||||
prev1, prev2 := *bootstrapDNS, *unpublishedDNS
|
||||
*bootstrapDNS = published
|
||||
*unpublishedDNS = unpublished
|
||||
t.Cleanup(func() {
|
||||
*bootstrapDNS = prev1
|
||||
*unpublishedDNS = prev2
|
||||
})
|
||||
|
||||
refreshBootstrapDNS()
|
||||
refreshUnpublishedDNS()
|
||||
|
||||
hasResponse := func(q string) bool {
|
||||
_, found := getBootstrapDNS(t, q)[q]
|
||||
return found
|
||||
}
|
||||
|
||||
if !hasResponse(published) {
|
||||
t.Errorf("expected response for: %s", published)
|
||||
}
|
||||
if !hasResponse(unpublished) {
|
||||
t.Errorf("expected response for: %s", unpublished)
|
||||
}
|
||||
|
||||
// Verify that querying for a random query or a real query does not
|
||||
// leak our unpublished domain
|
||||
m1 := getBootstrapDNS(t, published)
|
||||
if _, found := m1[unpublished]; found {
|
||||
t.Errorf("found unpublished domain %s: %+v", unpublished, m1)
|
||||
}
|
||||
m2 := getBootstrapDNS(t, "random.example.com")
|
||||
if _, found := m2[unpublished]; found {
|
||||
t.Errorf("found unpublished domain %s: %+v", unpublished, m2)
|
||||
}
|
||||
}
|
||||
|
||||
func resetMetrics() {
|
||||
publishedDNSHits.Set(0)
|
||||
publishedDNSMisses.Set(0)
|
||||
unpublishedDNSHits.Set(0)
|
||||
unpublishedDNSMisses.Set(0)
|
||||
}
|
||||
|
||||
// Verify that we don't count an empty list in the unpublishedDNSCache as a
|
||||
// cache hit in our metrics.
|
||||
func TestUnpublishedDNSEmptyList(t *testing.T) {
|
||||
pub := dnsEntryMap{
|
||||
"tailscale.com": {net.IPv4(10, 10, 10, 10)},
|
||||
}
|
||||
dnsCache.Store(pub)
|
||||
dnsCacheBytes.Store([]byte(`{"tailscale.com":["10.10.10.10"]}`))
|
||||
|
||||
unpublishedDNSCache.Store(dnsEntryMap{
|
||||
"log.tailscale.io": {},
|
||||
"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)},
|
||||
})
|
||||
|
||||
t.Run("CacheMiss", func(t *testing.T) {
|
||||
// One domain in map but empty, one not in map at all
|
||||
for _, q := range []string{"log.tailscale.io", "login.tailscale.com"} {
|
||||
resetMetrics()
|
||||
ips := getBootstrapDNS(t, q)
|
||||
|
||||
// Expected our public map to be returned on a cache miss
|
||||
if !reflect.DeepEqual(ips, pub) {
|
||||
t.Errorf("got ips=%+v; want %+v", ips, pub)
|
||||
}
|
||||
if v := unpublishedDNSHits.Value(); v != 0 {
|
||||
t.Errorf("got hits=%d; want 0", v)
|
||||
}
|
||||
if v := unpublishedDNSMisses.Value(); v != 1 {
|
||||
t.Errorf("got misses=%d; want 1", v)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Verify that we do get a valid response and metric.
|
||||
t.Run("CacheHit", func(t *testing.T) {
|
||||
resetMetrics()
|
||||
ips := getBootstrapDNS(t, "controlplane.tailscale.com")
|
||||
want := dnsEntryMap{"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)}}
|
||||
if !reflect.DeepEqual(ips, want) {
|
||||
t.Errorf("got ips=%+v; want %+v", ips, want)
|
||||
}
|
||||
if v := unpublishedDNSHits.Value(); v != 1 {
|
||||
t.Errorf("got hits=%d; want 1", v)
|
||||
}
|
||||
if v := unpublishedDNSMisses.Value(); v != 0 {
|
||||
t.Errorf("got misses=%d; want 0", v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,11 +20,6 @@ var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||
|
||||
type certProvider interface {
|
||||
// TLSConfig creates a new TLS config suitable for net/http.Server servers.
|
||||
//
|
||||
// The returned Config must have a GetCertificate function set and that
|
||||
// function must return a unique *tls.Certificate for each call. The
|
||||
// returned *tls.Certificate will be mutated by the caller to append to the
|
||||
// (*tls.Certificate).Certificate field.
|
||||
TLSConfig() *tls.Config
|
||||
// HTTPHandler handle ACME related request, if any.
|
||||
HTTPHandler(fallback http.Handler) http.Handler
|
||||
@@ -92,13 +87,7 @@ func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certif
|
||||
if hi.ServerName != m.hostname {
|
||||
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
||||
}
|
||||
|
||||
// Return a shallow copy of the cert so the caller can append to its
|
||||
// Certificate field.
|
||||
certCopy := new(tls.Certificate)
|
||||
*certCopy = *m.cert
|
||||
certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
|
||||
return certCopy, nil
|
||||
return m.cert, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
nhooyr.io/websocket from tailscale.com/cmd/derper+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/cmd/derper+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale
|
||||
tailscale.com/derp from tailscale.com/cmd/derper+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/derper
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/envknob from tailscale.com/derp+
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/metrics from tailscale.com/cmd/derper+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/stun from tailscale.com/cmd/derper
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tsweb from tailscale.com/cmd/derper
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnstate+
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert
|
||||
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2s from tailscale.com/tka
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/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 golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/cmd/derper+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from golang.org/x/crypto/acme+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/cmd/derper+
|
||||
flag from tailscale.com/cmd/derper
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
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/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptrace from net/http+
|
||||
net/http/internal from net/http
|
||||
net/http/pprof from tailscale.com/tsweb
|
||||
net/netip from go4.org/netipx+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
path from golang.org/x/crypto/acme/autocert+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from internal/profile+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/crypto/acme+
|
||||
runtime/pprof from net/http/pprof
|
||||
runtime/trace from net/http/pprof
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
@@ -14,18 +14,17 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
@@ -46,13 +45,11 @@ var (
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||
|
||||
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")
|
||||
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
|
||||
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
|
||||
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
|
||||
@@ -98,7 +95,7 @@ func loadConfig() config {
|
||||
}
|
||||
log.Printf("no config path specified; using %s", *configPath)
|
||||
}
|
||||
b, err := os.ReadFile(*configPath)
|
||||
b, err := ioutil.ReadFile(*configPath)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return writeNewConfig()
|
||||
@@ -154,7 +151,7 @@ func main() {
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
b, err := os.ReadFile(*meshPSKFile)
|
||||
b, err := ioutil.ReadFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -171,15 +168,9 @@ func main() {
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
if *runDERP {
|
||||
derpHandler := derphttp.Handler(s)
|
||||
derpHandler = addWebSocketSupport(s, derpHandler)
|
||||
mux.Handle("/derp", derpHandler)
|
||||
} else {
|
||||
mux.Handle("/derp", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "derp server disabled", http.StatusNotFound)
|
||||
}))
|
||||
}
|
||||
derpHandler := derphttp.Handler(s)
|
||||
derpHandler = addWebSocketSupport(s, derpHandler)
|
||||
mux.Handle("/derp", derpHandler)
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
@@ -195,17 +186,10 @@ func main() {
|
||||
server.
|
||||
</p>
|
||||
`)
|
||||
if !*runDERP {
|
||||
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
|
||||
}
|
||||
if tsweb.AllowDebugAccess(r) {
|
||||
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
|
||||
}
|
||||
}))
|
||||
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "User-agent: *\nDisallow: /\n")
|
||||
}))
|
||||
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent))
|
||||
debug := tsweb.Debugger(mux)
|
||||
debug.KV("TLS hostname", *hostname)
|
||||
debug.KV("Mesh key", s.HasMeshKey())
|
||||
@@ -223,11 +207,9 @@ func main() {
|
||||
go serveSTUN(listenHost, *stunPort)
|
||||
}
|
||||
|
||||
quietLogger := log.New(logFilter{}, "", 0)
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
ErrorLog: quietLogger,
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
|
||||
// Set read/write timeout. For derper, this basically
|
||||
// only affects TLS setup, as read/write deadlines are
|
||||
@@ -293,13 +275,9 @@ func main() {
|
||||
})
|
||||
if *httpPort > -1 {
|
||||
go func() {
|
||||
port80mux := http.NewServeMux()
|
||||
port80mux.HandleFunc("/generate_204", serveNoContent)
|
||||
port80mux.Handle("/", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
|
||||
Handler: port80mux,
|
||||
ErrorLog: quietLogger,
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
@@ -325,11 +303,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// For captive portal detection
|
||||
func serveNoContent(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -383,8 +356,7 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
|
||||
} else {
|
||||
stunIPv6.Add(1)
|
||||
}
|
||||
addr, _ := netip.AddrFromSlice(ua.IP)
|
||||
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port)))
|
||||
res := stun.Response(txid, ua.IP, uint16(ua.Port))
|
||||
_, err = pc.WriteTo(res, ua)
|
||||
if err != nil {
|
||||
stunWriteError.Add(1)
|
||||
@@ -475,22 +447,3 @@ func (l *rateLimitedListener) Accept() (net.Conn, error) {
|
||||
l.numAccepts.Add(1)
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
// logFilter is used to filter out useless error logs that are logged to
|
||||
// the net/http.Server.ErrorLog logger.
|
||||
type logFilter struct{}
|
||||
|
||||
func (logFilter) Write(p []byte) (int, error) {
|
||||
b := mem.B(p)
|
||||
if mem.HasSuffix(b, mem.S(": EOF\n")) ||
|
||||
mem.HasSuffix(b, mem.S(": i/o timeout\n")) ||
|
||||
mem.HasSuffix(b, mem.S(": read: connection reset by peer\n")) ||
|
||||
mem.HasSuffix(b, mem.S(": remote error: tls: bad certificate\n")) ||
|
||||
mem.HasSuffix(b, mem.S(": tls: first record does not look like a TLS handshake\n")) {
|
||||
// Skip this log message, but say that we processed it
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
log.Printf("%s", p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
@@ -33,12 +33,6 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
OriginPatterns: []string{"*"},
|
||||
// Disable compression because we transmit WireGuard messages that
|
||||
// are not compressible.
|
||||
// Additionally, Safari has a broken implementation of compression
|
||||
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||
// enabling it actively harmful.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket.Accept: %v", err)
|
||||
|
||||
@@ -360,7 +360,7 @@ func probeUDP(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (la
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
txBack, _, err := stun.ParseResponse(buf[:n])
|
||||
txBack, _, _, err := stun.ParseResponse(buf[:n])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing STUN response from %v: %v", ip, err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
@@ -31,14 +30,17 @@ var (
|
||||
cacheFname = rootFlagSet.String("cache-file", "./version-cache.json", "filename for the previous known version hash")
|
||||
timeout = rootFlagSet.Duration("timeout", 5*time.Minute, "timeout for the entire CI run")
|
||||
githubSyntax = rootFlagSet.Bool("github-syntax", true, "use GitHub Action error syntax (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message)")
|
||||
|
||||
modifiedExternallyFailure = make(chan struct{}, 1)
|
||||
)
|
||||
|
||||
func modifiedExternallyError() {
|
||||
if *githubSyntax {
|
||||
fmt.Printf("::warning file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
|
||||
fmt.Printf("::error file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
|
||||
} else {
|
||||
fmt.Printf("The policy file was modified externally in the admin console.\n")
|
||||
}
|
||||
modifiedExternallyFailure <- struct{}{}
|
||||
}
|
||||
|
||||
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
@@ -205,6 +207,10 @@ func main() {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(modifiedExternallyFailure) != 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func sumFile(fname string) (string, error) {
|
||||
@@ -265,16 +271,13 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
data, err := os.ReadFile(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err = hujson.Standardize(data)
|
||||
fin, err := os.Open(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), bytes.NewBuffer(data))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), fin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -105,7 +106,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
tmplData, err := os.ReadFile("hello.tmpl.html")
|
||||
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
|
||||
|
||||
@@ -63,19 +63,17 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// tailnet of connected node. When accessing shared nodes, this
|
||||
// will be empty because the tailnet of the sharee is not exposed.
|
||||
var tailnet string
|
||||
|
||||
if !info.Node.Hostinfo.ShareeNode() {
|
||||
var ok bool
|
||||
_, tailnet, ok = strings.Cut(info.Node.Name, info.Node.ComputedName+".")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
tailnet = strings.TrimSuffix(tailnet, ".beta.tailscale.net")
|
||||
_, tailnet, ok := strings.Cut(info.Node.Name, info.Node.ComputedName+".")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
tailnet, _, ok = strings.Cut(tailnet, ".beta.tailscale.net")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
|
||||
|
||||
@@ -29,7 +29,7 @@ var certCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("cert")
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output key file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
return fs
|
||||
})(),
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -169,8 +170,6 @@ change in the future.
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
netlockCmd,
|
||||
licensesCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
@@ -231,6 +230,8 @@ var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
var gotSignal atomic.Bool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
@@ -256,6 +257,7 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
||||
return
|
||||
}
|
||||
gotSignal.Set(true)
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
@@ -762,9 +762,6 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
case "NotepadURLs":
|
||||
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
||||
continue
|
||||
case "Egg":
|
||||
// Not applicable.
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
@@ -789,10 +786,6 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
|
||||
// It is used to test the --accept-risks flag.
|
||||
sshOverTailscale bool
|
||||
|
||||
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
|
||||
// updatePrefs might've mutated them (from applyImplicitPrefs).
|
||||
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
|
||||
@@ -920,159 +913,15 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable_ssh",
|
||||
flags: []string{"--ssh"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh",
|
||||
flags: []string{"--ssh=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to false")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
|
||||
runSSH: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh_over_ssh_no_risk",
|
||||
flags: []string{"--ssh=false"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
RunSSH: true,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "enable_ssh_over_ssh_no_risk",
|
||||
flags: []string{"--ssh=true"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "enable_ssh_over_ssh",
|
||||
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh_over_ssh",
|
||||
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to false")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.sshOverTailscale {
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
}
|
||||
if tt.env.goos == "" {
|
||||
tt.env.goos = "linux"
|
||||
}
|
||||
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
|
||||
flags := CleanUpArgs(tt.flags)
|
||||
if err := tt.env.flagSet.Parse(flags); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tt.env.flagSet.Parse(flags)
|
||||
|
||||
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
|
||||
if err != nil {
|
||||
@@ -1087,8 +936,6 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
} else if tt.wantErrSubtr != "" {
|
||||
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
|
||||
}
|
||||
if tt.checkUpdatePrefsMutations != nil {
|
||||
tt.checkUpdatePrefsMutations(t, newPrefs)
|
||||
@@ -1102,18 +949,13 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
||||
}
|
||||
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
|
||||
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func asJSON(v any) string {
|
||||
b, _ := json.MarshalIndent(v, "", "\t")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
@@ -62,9 +62,6 @@ func runConfigureHost(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("creating /dev/net/tun: %v, %s", err, out)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod("/dev/net", 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod("/dev/net/tun", 0666); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -489,15 +489,7 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := (&controlhttp.Dialer{
|
||||
Hostname: ts2021Args.host,
|
||||
HTTPPort: "80",
|
||||
HTTPSPort: "443",
|
||||
MachineKey: machinePrivate,
|
||||
ControlKey: keys.PublicKey,
|
||||
ProtocolVersion: uint16(ts2021Args.version),
|
||||
Dialer: dialFunc,
|
||||
}).Dial(ctx)
|
||||
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -22,13 +22,9 @@ var downCmd = &ffcli.Command{
|
||||
FlagSet: newDownFlagSet(),
|
||||
}
|
||||
|
||||
var downArgs struct {
|
||||
acceptedRisks string
|
||||
}
|
||||
|
||||
func newDownFlagSet() *flag.FlagSet {
|
||||
downf := newFlagSet("down")
|
||||
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
|
||||
registerAcceptRiskFlag(downf)
|
||||
return downf
|
||||
}
|
||||
|
||||
@@ -38,7 +34,7 @@ func runDown(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if isSSHOverTailscale() {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
||||
var licensesCmd = &ffcli.Command{
|
||||
Name: "licenses",
|
||||
ShortUsage: "licenses",
|
||||
ShortHelp: "Get open source license information",
|
||||
LongHelp: "Get open source license information",
|
||||
Exec: runLicenses,
|
||||
}
|
||||
|
||||
// licensesURL returns the absolute URL containing open source license information for the current platform.
|
||||
func licensesURL() string {
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
return "https://tailscale.com/licenses/android"
|
||||
case "darwin", "ios":
|
||||
return "https://tailscale.com/licenses/apple"
|
||||
case "windows":
|
||||
return "https://tailscale.com/licenses/windows"
|
||||
default:
|
||||
return "https://tailscale.com/licenses/tailscale"
|
||||
}
|
||||
}
|
||||
|
||||
func runLicenses(ctx context.Context, args []string) error {
|
||||
licenses := licensesURL()
|
||||
outln(`
|
||||
Tailscale wouldn't be possible without the contributions of thousands of open
|
||||
source developers. To see the open source packages included in Tailscale and
|
||||
their respective license information, visit:
|
||||
|
||||
` + licenses)
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -201,7 +202,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var netlockCmd = &ffcli.Command{
|
||||
Name: "lock",
|
||||
ShortUsage: "lock <sub-command> <arguments>",
|
||||
ShortHelp: "Manipulate the tailnet key authority",
|
||||
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
|
||||
Exec: runNetworkLockStatus,
|
||||
}
|
||||
|
||||
var nlInitCmd = &ffcli.Command{
|
||||
Name: "init",
|
||||
ShortUsage: "init <public-key>...",
|
||||
ShortHelp: "Initialize the tailnet key authority",
|
||||
Exec: runNetworkLockInit,
|
||||
}
|
||||
|
||||
func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
st, err := localClient.NetworkLockStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if st.Enabled {
|
||||
return errors.New("network-lock is already enabled")
|
||||
}
|
||||
|
||||
// Parse the set of initially-trusted keys.
|
||||
// Keys are specified using their key.NLPublic.MarshalText representation,
|
||||
// with an optional '?<votes>' suffix.
|
||||
var keys []tka.Key
|
||||
for i, a := range args {
|
||||
var key key.NLPublic
|
||||
spl := strings.SplitN(a, "?", 2)
|
||||
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
|
||||
return fmt.Errorf("parsing key %d: %v", i+1, err)
|
||||
}
|
||||
|
||||
k := tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Public: key.Verifier(),
|
||||
Votes: 1,
|
||||
}
|
||||
if len(spl) > 1 {
|
||||
votes, err := strconv.Atoi(spl[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
||||
}
|
||||
k.Votes = uint(votes)
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
status, err := localClient.NetworkLockInit(ctx, keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Status: %+v\n\n", status)
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlStatusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status",
|
||||
ShortHelp: "Outputs the state of network lock",
|
||||
Exec: runNetworkLockStatus,
|
||||
}
|
||||
|
||||
func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
st, err := localClient.NetworkLockStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if st.Enabled {
|
||||
fmt.Println("Network-lock is ENABLED.")
|
||||
} else {
|
||||
fmt.Println("Network-lock is NOT enabled.")
|
||||
}
|
||||
p, err := st.PublicKey.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("our public-key: %s\n", p)
|
||||
return nil
|
||||
}
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
riskTypes []string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
riskTypes []string
|
||||
acceptedRisks string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
)
|
||||
|
||||
func registerRiskType(riskType string) string {
|
||||
@@ -27,13 +28,12 @@ func registerRiskType(riskType string) string {
|
||||
|
||||
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
|
||||
// in presentRiskToUser.
|
||||
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
|
||||
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
|
||||
func registerAcceptRiskFlag(f *flag.FlagSet) {
|
||||
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
|
||||
}
|
||||
|
||||
// isRiskAccepted reports whether riskType is in the comma-separated list of
|
||||
// risks in acceptedRisks.
|
||||
func isRiskAccepted(riskType, acceptedRisks string) bool {
|
||||
// riskAccepted reports whether riskType is in acceptedRisks.
|
||||
func riskAccepted(riskType string) bool {
|
||||
for _, r := range strings.Split(acceptedRisks, ",") {
|
||||
if r == riskType {
|
||||
return true
|
||||
@@ -49,16 +49,12 @@ var errAborted = errors.New("aborted, no changes made")
|
||||
// It is used by the presentRiskToUser function below.
|
||||
const riskAbortTimeSeconds = 5
|
||||
|
||||
// presentRiskToUser displays the risk message and waits for the user to cancel.
|
||||
// It returns errorAborted if the user aborts. In tests it returns errAborted
|
||||
// immediately unless the risk has been explicitly accepted.
|
||||
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
|
||||
if isRiskAccepted(riskType, acceptedRisks) {
|
||||
// presentRiskToUser displays the risk message and waits for the user to
|
||||
// cancel. It returns errorAborted if the user aborts.
|
||||
func presentRiskToUser(riskType, riskMessage string) error {
|
||||
if riskAccepted(riskType) {
|
||||
return nil
|
||||
}
|
||||
if inTest() {
|
||||
return errAborted
|
||||
}
|
||||
outln(riskMessage)
|
||||
printf("To skip this warning, use --accept-risk=%s\n", riskType)
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
|
||||
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
|
||||
registerAcceptRiskFlag(upf)
|
||||
return upf
|
||||
}
|
||||
|
||||
@@ -150,7 +150,6 @@ type upArgsT struct {
|
||||
opUser string
|
||||
json bool
|
||||
timeout time.Duration
|
||||
acceptedRisks string
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -253,7 +252,7 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
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 IPv4 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if advertiseDefaultRoute {
|
||||
@@ -377,21 +376,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
// Do this after validations to avoid the 5s delay if we're going to error
|
||||
// out anyway.
|
||||
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
||||
fmt.Println("wantSSH", wantSSH, "haveSSH", haveSSH)
|
||||
if wantSSH != haveSSH && isSSHOverTailscale() {
|
||||
if wantSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
@@ -422,12 +406,8 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
var egg bool
|
||||
if len(args) > 0 {
|
||||
egg = fmt.Sprint(args) == "[up down down left right left right b a]"
|
||||
if !egg {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
@@ -491,6 +471,17 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
|
||||
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
|
||||
if upArgs.runSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
checkSSHUpWarnings(ctx)
|
||||
@@ -502,7 +493,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
if justEditMP != nil {
|
||||
justEditMP.EggSet = true
|
||||
_, err := localClient.EditPrefs(ctx, justEditMP)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -58,7 +59,6 @@ type tmplData struct {
|
||||
IP string
|
||||
AdvertiseExitNode bool
|
||||
AdvertiseRoutes string
|
||||
LicensesURL string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -253,7 +253,7 @@ func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
out, err := io.ReadAll(resp.Body)
|
||||
out, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -392,7 +392,6 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Profile: profile,
|
||||
Status: st.BackendState,
|
||||
DeviceName: deviceName,
|
||||
LicensesURL: licensesURL(),
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</head>
|
||||
|
||||
<body class="py-14">
|
||||
<main class="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<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">
|
||||
@@ -100,9 +100,6 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
<footer class="container max-w-lg mx-auto text-center">
|
||||
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
|
||||
</footer>
|
||||
<script>(function () {
|
||||
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||
let fetchingUrl = false;
|
||||
|
||||
@@ -64,7 +64,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
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+
|
||||
@@ -72,9 +71,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 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/netcheck+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/types/key
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
@@ -90,7 +89,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
@@ -99,9 +97,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -118,15 +114,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.org/x/net/icmp+
|
||||
golang.org/x/net/ipv6 from golang.org/x/net/icmp
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -172,7 +173,7 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/publicdns from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
@@ -227,7 +227,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
@@ -245,7 +244,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tka from tailscale.com/types/key+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
@@ -264,7 +263,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/tkatype from tailscale.com/tka+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
@@ -273,7 +271,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
@@ -281,7 +278,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
@@ -290,13 +286,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/wgengine/wgint 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/acme from tailscale.com/ipn/localapi
|
||||
@@ -317,7 +312,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
|
||||
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
@@ -325,9 +320,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
@@ -405,7 +399,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptest from tailscale.com/control/controlclient
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/httputil from github.com/aws/smithy-go/transport/http+
|
||||
net/http/internal from net/http+
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -141,7 +142,7 @@ func installSystemDaemonDarwin(args []string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -98,20 +97,6 @@ func defaultTunName() string {
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
// defaultPort returns the default UDP port to listen on for disco+wireguard.
|
||||
// By default it returns 0, to pick one randomly from the kernel.
|
||||
// If the environment variable PORT is set, that's used instead.
|
||||
// The PORT environment variable is chosen to match what the Linux systemd
|
||||
// unit uses, to make documentation more consistent.
|
||||
func defaultPort() uint16 {
|
||||
if s := envknob.String("PORT"); s != "" {
|
||||
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
|
||||
return uint16(p)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var args struct {
|
||||
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
||||
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
||||
@@ -128,7 +113,6 @@ var args struct {
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
httpProxyAddr string // listen address for HTTP proxy server
|
||||
disableLogs bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -147,9 +131,6 @@ var subCommands = map[string]*func([]string) error{
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
|
||||
func main() {
|
||||
envknob.PanicIfAnyEnvCheckedInInit()
|
||||
envknob.ApplyDiskConfig()
|
||||
|
||||
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")
|
||||
@@ -157,13 +138,12 @@ func main() {
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||
|
||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
||||
beCLI()
|
||||
@@ -219,10 +199,6 @@ func main() {
|
||||
args.statepath = paths.DefaultTailscaledStateFile()
|
||||
}
|
||||
|
||||
if args.disableLogs {
|
||||
envknob.SetNoLogsNoSupport()
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
}
|
||||
@@ -326,10 +302,6 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
if isWindowsService() {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
@@ -398,7 +370,7 @@ func run() error {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
@@ -499,6 +471,8 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
|
||||
return nil, false, multierr.New(errs...)
|
||||
}
|
||||
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
|
||||
return v
|
||||
@@ -507,7 +481,7 @@ func shouldWrapNetstack() bool {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin", "freebsd", "openbsd":
|
||||
case "windows", "darwin", "freebsd":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
return true
|
||||
@@ -569,7 +543,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if shouldWrapNetstack() {
|
||||
if wrapNetstack {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +197,6 @@ func beWindowsSubprocess() bool {
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
@@ -277,7 +274,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
dev.Close()
|
||||
return nil, nil, fmt.Errorf("router: %w", err)
|
||||
}
|
||||
if shouldWrapNetstack() {
|
||||
if wrapNetstack {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
@@ -304,7 +301,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = false
|
||||
ns.ProcessSubnets = shouldWrapNetstack()
|
||||
ns.ProcessSubnets = wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
5
cmd/tsconnect/.gitignore
vendored
5
cmd/tsconnect/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
src/wasm_exec.js
|
||||
src/main.wasm
|
||||
node_modules/
|
||||
/dist
|
||||
/pkg
|
||||
dist/
|
||||
|
||||
@@ -28,22 +28,3 @@ To serve them, run:
|
||||
```
|
||||
|
||||
By default the build output is placed in the `dist/` directory and embedded in the binary, but this can be controlled by the `-distdir` flag. The `-addr` flag controls the interface and port that the serve listens on.
|
||||
|
||||
# Library / NPM Package
|
||||
|
||||
The client is also available as an NPM package. To build it, run:
|
||||
|
||||
```
|
||||
./tool/go run ./cmd/tsconnect build-pkg
|
||||
```
|
||||
|
||||
That places the output in the `pkg/` directory, which may then be uploaded to a package registry (or installed from the file path directly).
|
||||
|
||||
To do two-sided development (on both the NPM package and code that uses it), run:
|
||||
|
||||
```
|
||||
./tool/go run ./cmd/tsconnect dev-pkg
|
||||
|
||||
```
|
||||
|
||||
This serves the module at http://localhost:9090/pkg/pkg.js and the generated wasm file at http://localhost:9090/pkg/main.wasm. The two files can be used as drop-in replacements for normal imports of the NPM module.
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func runBuildPkg() {
|
||||
buildOptions, err := commonPkgSetup(prodMode)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot setup: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Linting...\n")
|
||||
if err := runYarn("lint"); err != nil {
|
||||
log.Fatalf("Linting failed: %v", err)
|
||||
}
|
||||
|
||||
if err := cleanDir(*pkgDir, "package.json"); err != nil {
|
||||
log.Fatalf("Cannot clean %s: %v", *pkgDir, err)
|
||||
}
|
||||
|
||||
buildOptions.Write = true
|
||||
buildOptions.MinifyWhitespace = true
|
||||
buildOptions.MinifyIdentifiers = true
|
||||
buildOptions.MinifySyntax = true
|
||||
|
||||
runEsbuild(*buildOptions)
|
||||
|
||||
log.Printf("Generating types...\n")
|
||||
if err := runYarn("pkg-types"); err != nil {
|
||||
log.Fatalf("Type generation failed: %v", err)
|
||||
}
|
||||
|
||||
if err := updateVersion(); err != nil {
|
||||
log.Fatalf("Cannot update version: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Built package version %s", version.Long)
|
||||
}
|
||||
|
||||
func updateVersion() error {
|
||||
packageJSONBytes, err := os.ReadFile("package.json.tmpl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not read package.json: %w", err)
|
||||
}
|
||||
|
||||
var packageJSON map[string]any
|
||||
packageJSONBytes, err = hujson.Standardize(packageJSONBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not standardize template package.json: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(packageJSONBytes, &packageJSON); err != nil {
|
||||
return fmt.Errorf("Could not unmarshal package.json: %w", err)
|
||||
}
|
||||
packageJSON["version"] = version.Long
|
||||
|
||||
packageJSONBytes, err = json.MarshalIndent(packageJSON, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not marshal package.json: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(path.Join(*pkgDir, "package.json"), packageJSONBytes, 0644)
|
||||
}
|
||||
@@ -7,11 +7,13 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"tailscale.com/util/precompress"
|
||||
)
|
||||
|
||||
@@ -26,7 +28,7 @@ func runBuild() {
|
||||
log.Fatalf("Linting failed: %v", err)
|
||||
}
|
||||
|
||||
if err := cleanDir(*distDir, "placeholder"); err != nil {
|
||||
if err := cleanDist(); err != nil {
|
||||
log.Fatalf("Cannot clean %s: %v", *distDir, err)
|
||||
}
|
||||
|
||||
@@ -39,14 +41,28 @@ func runBuild() {
|
||||
buildOptions.AssetNames = "[name]-[hash]"
|
||||
buildOptions.Metafile = true
|
||||
|
||||
result := runEsbuild(*buildOptions)
|
||||
log.Printf("Running esbuild...\n")
|
||||
result := esbuild.Build(*buildOptions)
|
||||
if len(result.Errors) > 0 {
|
||||
log.Printf("ESBuild Error:\n")
|
||||
for _, e := range result.Errors {
|
||||
log.Printf("%v", e)
|
||||
}
|
||||
log.Fatal("Build failed")
|
||||
}
|
||||
if len(result.Warnings) > 0 {
|
||||
log.Printf("ESBuild Warnings:\n")
|
||||
for _, w := range result.Warnings {
|
||||
log.Printf("%v", w)
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve build metadata so we can extract hashed file names for serving.
|
||||
metadataBytes, err := fixEsbuildMetadataPaths(result.Metafile)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
|
||||
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
|
||||
log.Fatalf("Cannot write metadata: %v", err)
|
||||
}
|
||||
|
||||
@@ -82,6 +98,8 @@ func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
|
||||
return json.Marshal(metadata)
|
||||
}
|
||||
|
||||
// cleanDist removes files from the dist build directory, except the placeholder
|
||||
// one that we keep to make sure Git still creates the directory.
|
||||
func cleanDist() error {
|
||||
log.Printf("Cleaning %s...\n", *distDir)
|
||||
files, err := os.ReadDir(*distDir)
|
||||
|
||||
@@ -7,7 +7,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
"time"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,185 +32,73 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
|
||||
return nil, fmt.Errorf("Cannot change cwd: %w", err)
|
||||
}
|
||||
}
|
||||
if err := installJSDeps(); err != nil {
|
||||
return nil, fmt.Errorf("Cannot install JS deps: %w", err)
|
||||
if err := buildDeps(dev); err != nil {
|
||||
return nil, fmt.Errorf("Cannot build deps: %w", err)
|
||||
}
|
||||
|
||||
return &esbuild.BuildOptions{
|
||||
EntryPoints: []string{"src/app/index.ts", "src/app/index.css"},
|
||||
EntryPoints: []string{"src/index.ts", "src/index.css"},
|
||||
Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
|
||||
Outdir: *distDir,
|
||||
Bundle: true,
|
||||
Sourcemap: esbuild.SourceMapLinked,
|
||||
LogLevel: esbuild.LogLevelInfo,
|
||||
Define: map[string]string{"DEBUG": strconv.FormatBool(dev)},
|
||||
Target: esbuild.ES2017,
|
||||
Plugins: []esbuild.Plugin{
|
||||
{
|
||||
Name: "tailscale-tailwind",
|
||||
Setup: func(build esbuild.PluginBuild) {
|
||||
setupEsbuildTailwind(build, dev)
|
||||
},
|
||||
Plugins: []esbuild.Plugin{{
|
||||
Name: "tailscale-tailwind",
|
||||
Setup: func(build esbuild.PluginBuild) {
|
||||
setupEsbuildTailwind(build, dev)
|
||||
},
|
||||
{
|
||||
Name: "tailscale-go-wasm-exec-js",
|
||||
Setup: setupEsbuildWasmExecJS,
|
||||
},
|
||||
{
|
||||
Name: "tailscale-wasm",
|
||||
Setup: func(build esbuild.PluginBuild) {
|
||||
setupEsbuildWasm(build, dev)
|
||||
},
|
||||
},
|
||||
},
|
||||
JSXMode: esbuild.JSXModeAutomatic,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func commonPkgSetup(dev bool) (*esbuild.BuildOptions, error) {
|
||||
buildOptions, err := commonSetup(dev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// buildDeps builds the static assets that are needed for the server (except for
|
||||
// JS/CSS bundling, which is handled by esbuild).
|
||||
func buildDeps(dev bool) error {
|
||||
if err := copyWasmExec(); err != nil {
|
||||
return fmt.Errorf("Cannot copy wasm_exec.js: %w", err)
|
||||
}
|
||||
buildOptions.EntryPoints = []string{"src/pkg/pkg.ts", "src/pkg/pkg.css"}
|
||||
buildOptions.Outdir = *pkgDir
|
||||
buildOptions.Format = esbuild.FormatESModule
|
||||
buildOptions.AssetNames = "[name]"
|
||||
return buildOptions, nil
|
||||
}
|
||||
|
||||
// cleanDir removes files from dirPath, except the ones specified by
|
||||
// preserveFiles.
|
||||
func cleanDir(dirPath string, preserveFiles ...string) error {
|
||||
log.Printf("Cleaning %s...\n", dirPath)
|
||||
files, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(dirPath, 0755)
|
||||
}
|
||||
return err
|
||||
if err := buildWasm(dev); err != nil {
|
||||
return fmt.Errorf("Cannot build main.wasm: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !slices.Contains(preserveFiles, file.Name()) {
|
||||
if err := os.Remove(filepath.Join(dirPath, file.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := installJSDeps(); err != nil {
|
||||
return fmt.Errorf("Cannot install JS deps: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runEsbuildServe(buildOptions esbuild.BuildOptions) {
|
||||
host, portStr, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse addr: %v", err)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse port: %v", err)
|
||||
}
|
||||
result, err := esbuild.Serve(esbuild.ServeOptions{
|
||||
Port: uint16(port),
|
||||
Host: host,
|
||||
Servedir: "./",
|
||||
}, buildOptions)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot start esbuild server: %v", err)
|
||||
}
|
||||
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
|
||||
result.Wait()
|
||||
}
|
||||
|
||||
func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {
|
||||
log.Printf("Running esbuild...\n")
|
||||
result := esbuild.Build(buildOptions)
|
||||
if len(result.Errors) > 0 {
|
||||
log.Printf("ESBuild Error:\n")
|
||||
for _, e := range result.Errors {
|
||||
log.Printf("%v", e)
|
||||
}
|
||||
log.Fatal("Build failed")
|
||||
}
|
||||
if len(result.Warnings) > 0 {
|
||||
log.Printf("ESBuild Warnings:\n")
|
||||
for _, w := range result.Warnings {
|
||||
log.Printf("%v", w)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// setupEsbuildWasmExecJS generates an esbuild plugin that serves the current
|
||||
// wasm_exec.js runtime helper library from the Go toolchain.
|
||||
func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
|
||||
// copyWasmExec grabs the current wasm_exec.js runtime helper library from the
|
||||
// Go toolchain.
|
||||
func copyWasmExec() error {
|
||||
log.Printf("Copying wasm_exec.js...\n")
|
||||
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
|
||||
build.OnResolve(esbuild.OnResolveOptions{
|
||||
Filter: "./wasm_exec$",
|
||||
}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
|
||||
return esbuild.OnResolveResult{Path: wasmExecSrcPath}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// setupEsbuildWasm generates an esbuild plugin that builds the Tailscale wasm
|
||||
// binary and serves it as a file that the JS can load.
|
||||
func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
|
||||
// Add a resolve hook to convince esbuild that the path exists.
|
||||
build.OnResolve(esbuild.OnResolveOptions{
|
||||
Filter: "./main.wasm$",
|
||||
}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
|
||||
return esbuild.OnResolveResult{
|
||||
Path: "./src/main.wasm",
|
||||
Namespace: "generated",
|
||||
}, nil
|
||||
})
|
||||
build.OnLoad(esbuild.OnLoadOptions{
|
||||
Filter: "./src/main.wasm$",
|
||||
}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
|
||||
contents, err := buildWasm(dev)
|
||||
if err != nil {
|
||||
return esbuild.OnLoadResult{}, fmt.Errorf("Cannot build main.wasm: %w", err)
|
||||
}
|
||||
contentsStr := string(contents)
|
||||
return esbuild.OnLoadResult{
|
||||
Contents: &contentsStr,
|
||||
Loader: esbuild.LoaderFile,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func buildWasm(dev bool) ([]byte, error) {
|
||||
start := time.Now()
|
||||
outputFile, err := os.CreateTemp("", "main.*.wasm")
|
||||
wasmExecDstPath := filepath.Join("src", "wasm_exec.js")
|
||||
contents, err := os.ReadFile(wasmExecSrcPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
|
||||
return err
|
||||
}
|
||||
outputPath := outputFile.Name()
|
||||
defer os.Remove(outputPath)
|
||||
return os.WriteFile(wasmExecDstPath, contents, 0600)
|
||||
}
|
||||
|
||||
// buildWasm builds the Tailscale wasm binary and places it where the JS can
|
||||
// load it.
|
||||
func buildWasm(dev bool) error {
|
||||
log.Printf("Building wasm...\n")
|
||||
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
|
||||
if !dev {
|
||||
if *devControl != "" {
|
||||
return nil, fmt.Errorf("Development control URL can only be used in dev mode.")
|
||||
}
|
||||
// Omit long paths and debug symbols in release builds, to reduce the
|
||||
// generated WASM binary size.
|
||||
args = append(args, "-trimpath", "-ldflags", "-s -w")
|
||||
} else if *devControl != "" {
|
||||
args = append(args, "-ldflags", fmt.Sprintf("-X 'main.ControlURL=%v'", *devControl))
|
||||
}
|
||||
|
||||
args = append(args, "-o", outputPath, "./wasm")
|
||||
args = append(args, "-o", "src/main.wasm", "./wasm")
|
||||
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
|
||||
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot build main.wasm: %w", err)
|
||||
}
|
||||
log.Printf("Built wasm in %v\n", time.Since(start))
|
||||
return os.ReadFile(outputPath)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// installJSDeps installs the JavaScript dependencies specified by package.json
|
||||
@@ -242,7 +128,7 @@ type EsbuildMetadata struct {
|
||||
|
||||
func setupEsbuildTailwind(build esbuild.PluginBuild, dev bool) {
|
||||
build.OnLoad(esbuild.OnLoadOptions{
|
||||
Filter: "./src/.*\\.css$",
|
||||
Filter: "./src/index.css$",
|
||||
}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
|
||||
start := time.Now()
|
||||
yarnArgs := []string{"--silent", "tailwind", "-i", args.Path}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
func runDevPkg() {
|
||||
buildOptions, err := commonPkgSetup(devMode)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot setup: %v", err)
|
||||
}
|
||||
runEsbuildServe(*buildOptions)
|
||||
}
|
||||
@@ -6,6 +6,10 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
)
|
||||
|
||||
func runDev() {
|
||||
@@ -13,5 +17,22 @@ func runDev() {
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot setup: %v", err)
|
||||
}
|
||||
runEsbuildServe(*buildOptions)
|
||||
host, portStr, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse addr: %v", err)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse port: %v", err)
|
||||
}
|
||||
result, err := esbuild.Serve(esbuild.ServeOptions{
|
||||
Port: uint16(port),
|
||||
Host: host,
|
||||
Servedir: "./",
|
||||
}, *buildOptions)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot start esbuild server: %v", err)
|
||||
}
|
||||
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
|
||||
result.Wait()
|
||||
}
|
||||
|
||||
@@ -8,13 +8,37 @@
|
||||
<script src="dist/index.js" defer></script>
|
||||
</head>
|
||||
<body class="flex flex-col h-screen overflow-hidden">
|
||||
<!-- Placeholder so that we don't have an empty page while the JS loads.
|
||||
It should match the markup generated by Header component. -->
|
||||
<div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
|
||||
<header class="container mx-auto px-4 flex flex-row items-center">
|
||||
<h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
|
||||
<div class="text-gray-600">Loading…</div>
|
||||
<div class="text-gray-600" id="state">Loading…</div>
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
id="content"
|
||||
class="flex-grow flex flex-col justify-center overflow-hidden"
|
||||
>
|
||||
<form
|
||||
id="ssh-form"
|
||||
class="container mx-auto px-4 hidden flex justify-center"
|
||||
>
|
||||
<input type="text" class="input username" placeholder="Username" />
|
||||
<div class="select-with-arrow mx-2">
|
||||
<select class="select"></select>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
|
||||
value="SSH"
|
||||
/>
|
||||
</form>
|
||||
<div id="no-ssh" class="container mx-auto px-4 hidden text-center">
|
||||
None of your machines have
|
||||
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link"
|
||||
>Tailscale SSH</a
|
||||
>
|
||||
enabled. Give it a try!
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,18 +5,14 @@
|
||||
"devDependencies": {
|
||||
"@types/golang-wasm-exec": "^1.15.0",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"dts-bundle-generator": "^6.12.0",
|
||||
"preact": "^10.10.0",
|
||||
"qrcode": "^1.5.0",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"typescript": "^4.7.4",
|
||||
"xterm": "5.0.0-beta.58",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-web-links": "0.7.0-beta.6"
|
||||
"xterm": "^4.18.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"pkg-types": "dts-bundle-generator --inline-declare-global=true --no-banner -o pkg/pkg.d.ts src/pkg/pkg.ts"
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Template for the package.json that is generated by the build-pkg command.
|
||||
// The version number will be replaced by the current Tailscale client version
|
||||
// number.
|
||||
{
|
||||
"author": "Tailscale Inc.",
|
||||
"description": "Tailscale Connect SDK",
|
||||
"license": "BSD-3-Clause",
|
||||
"name": "tailscale-connect",
|
||||
"type": "module",
|
||||
"main": "./pkg.js",
|
||||
"types": "./pkg.d.ts",
|
||||
"version": "AUTO_GENERATED"
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -74,7 +75,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
|
||||
}
|
||||
defer esbuildMetadataFile.Close()
|
||||
esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
|
||||
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
|
||||
}
|
||||
@@ -114,8 +115,8 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
}
|
||||
|
||||
var entryPointsToDefaultDistPaths = map[string]string{
|
||||
"src/app/index.css": "dist/index.css",
|
||||
"src/app/index.ts": "dist/index.js",
|
||||
"src/index.css": "dist/index.css",
|
||||
"src/index.ts": "dist/index.js",
|
||||
}
|
||||
|
||||
func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import { render, Component } from "preact"
|
||||
import { URLDisplay } from "./url-display"
|
||||
import { Header } from "./header"
|
||||
import { GoPanicDisplay } from "./go-panic-display"
|
||||
import { SSH } from "./ssh"
|
||||
|
||||
type AppState = {
|
||||
ipn?: IPN
|
||||
ipnState: IPNState
|
||||
netMap?: IPNNetMap
|
||||
browseToURL?: string
|
||||
goPanicError?: string
|
||||
}
|
||||
|
||||
class App extends Component<{}, AppState> {
|
||||
state: AppState = { ipnState: "NoState" }
|
||||
#goPanicTimeout?: number
|
||||
|
||||
render() {
|
||||
const { ipn, ipnState, goPanicError, netMap, browseToURL } = this.state
|
||||
|
||||
let goPanicDisplay
|
||||
if (goPanicError) {
|
||||
goPanicDisplay = (
|
||||
<GoPanicDisplay error={goPanicError} dismiss={this.clearGoPanic} />
|
||||
)
|
||||
}
|
||||
|
||||
let urlDisplay
|
||||
if (browseToURL) {
|
||||
urlDisplay = <URLDisplay url={browseToURL} />
|
||||
}
|
||||
|
||||
let machineAuthInstructions
|
||||
if (ipnState === "NeedsMachineAuth") {
|
||||
machineAuthInstructions = (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
An administrator needs to authorize this device.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let ssh
|
||||
if (ipn && ipnState === "Running" && netMap) {
|
||||
ssh = <SSH netMap={netMap} ipn={ipn} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header state={ipnState} ipn={ipn} />
|
||||
{goPanicDisplay}
|
||||
<div class="flex-grow flex flex-col justify-center overflow-hidden">
|
||||
{urlDisplay}
|
||||
{machineAuthInstructions}
|
||||
{ssh}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
runWithIPN(ipn: IPN) {
|
||||
this.setState({ ipn }, () => {
|
||||
ipn.run({
|
||||
notifyState: this.handleIPNState,
|
||||
notifyNetMap: this.handleNetMap,
|
||||
notifyBrowseToURL: this.handleBrowseToURL,
|
||||
notifyPanicRecover: this.handleGoPanic,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleIPNState = (state: IPNState) => {
|
||||
const { ipn } = this.state
|
||||
this.setState({ ipnState: state })
|
||||
if (state === "NeedsLogin") {
|
||||
ipn?.login()
|
||||
} else if (["Running", "NeedsMachineAuth"].includes(state)) {
|
||||
this.setState({ browseToURL: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
handleNetMap = (netMapStr: string) => {
|
||||
const netMap = JSON.parse(netMapStr) as IPNNetMap
|
||||
if (DEBUG) {
|
||||
console.log("Received net map: " + JSON.stringify(netMap, null, 2))
|
||||
}
|
||||
this.setState({ netMap })
|
||||
}
|
||||
|
||||
handleBrowseToURL = (url: string) => {
|
||||
if (this.state.ipnState === "Running") {
|
||||
// Ignore URL requests if we're already running -- it's most likely an
|
||||
// SSH check mode trigger and we already linkify the displayed URL
|
||||
// in the terminal.
|
||||
return
|
||||
}
|
||||
this.setState({ browseToURL: url })
|
||||
}
|
||||
|
||||
handleGoPanic = (error: string) => {
|
||||
if (DEBUG) {
|
||||
console.error("Go panic", error)
|
||||
}
|
||||
this.setState({ goPanicError: error })
|
||||
if (this.#goPanicTimeout) {
|
||||
window.clearTimeout(this.#goPanicTimeout)
|
||||
}
|
||||
this.#goPanicTimeout = window.setTimeout(this.clearGoPanic, 10000)
|
||||
}
|
||||
|
||||
clearGoPanic = () => {
|
||||
window.clearTimeout(this.#goPanicTimeout)
|
||||
this.#goPanicTimeout = undefined
|
||||
this.setState({ goPanicError: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
export function renderApp(): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
render(
|
||||
<App ref={(app) => (app ? resolve(app) : undefined)} />,
|
||||
document.body
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
export function GoPanicDisplay({
|
||||
error,
|
||||
dismiss,
|
||||
}: {
|
||||
error: string
|
||||
dismiss: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="rounded bg-red-500 p-2 absolute top-2 right-2 text-white font-bold text-right cursor-pointer"
|
||||
onClick={dismiss}
|
||||
>
|
||||
Tailscale has encountered an error.
|
||||
<div class="text-sm font-normal">Click to reload</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
|
||||
const stateText = STATE_LABELS[state]
|
||||
|
||||
let logoutButton
|
||||
if (state === "Running") {
|
||||
logoutButton = (
|
||||
<button
|
||||
class="button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
|
||||
onClick={() => ipn?.logout()}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
|
||||
<header class="container mx-auto px-4 flex flex-row items-center">
|
||||
<h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
|
||||
<div class="text-gray-600">{stateText}</div>
|
||||
{logoutButton}
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const STATE_LABELS = {
|
||||
NoState: "Initializing…",
|
||||
InUseOtherUser: "In-use by another user",
|
||||
NeedsLogin: "Needs login",
|
||||
NeedsMachineAuth: "Needs authorization",
|
||||
Stopped: "Stopped",
|
||||
Starting: "Starting…",
|
||||
Running: "Running",
|
||||
} as const
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import "../wasm_exec"
|
||||
import wasmUrl from "./main.wasm"
|
||||
import { sessionStateStorage } from "../lib/js-state-store"
|
||||
import { renderApp } from "./app"
|
||||
|
||||
async function main() {
|
||||
const app = await renderApp()
|
||||
const go = new Go()
|
||||
const wasmInstance = await WebAssembly.instantiateStreaming(
|
||||
fetch(`./dist/${wasmUrl}`),
|
||||
go.importObject
|
||||
)
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
go.run(wasmInstance.instance).then(() =>
|
||||
app.handleGoPanic("Unexpected shutdown")
|
||||
)
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const authKey = params.get("authkey") ?? undefined
|
||||
|
||||
const ipn = newIPN({
|
||||
// Persist IPN state in sessionStorage in development, so that we don't need
|
||||
// to re-authorize every time we reload the page.
|
||||
stateStorage: DEBUG ? sessionStateStorage : undefined,
|
||||
// authKey allows for an auth key to be
|
||||
// specified as a url param which automatically
|
||||
// authorizes the client for use.
|
||||
authKey: DEBUG ? authKey : undefined,
|
||||
})
|
||||
app.runWithIPN(ipn)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,153 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks"
|
||||
import { createPortal } from "preact/compat"
|
||||
import type { VNode } from "preact"
|
||||
import { runSSHSession, SSHSessionDef } from "../lib/ssh"
|
||||
|
||||
export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
|
||||
const [sshSessionDef, setSSHSessionDef] = useState<SSHFormSessionDef | null>(
|
||||
null
|
||||
)
|
||||
const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
|
||||
if (sshSessionDef) {
|
||||
const sshSession = (
|
||||
<SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
|
||||
)
|
||||
if (sshSessionDef.newWindow) {
|
||||
return <NewWindow close={clearSSHSessionDef}>{sshSession}</NewWindow>
|
||||
}
|
||||
return sshSession
|
||||
}
|
||||
const sshPeers = netMap.peers.filter(
|
||||
(p) => p.tailscaleSSHEnabled && p.online !== false
|
||||
)
|
||||
|
||||
if (sshPeers.length == 0) {
|
||||
return <NoSSHPeers />
|
||||
}
|
||||
|
||||
return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} />
|
||||
}
|
||||
|
||||
type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
|
||||
|
||||
function SSHSession({
|
||||
def,
|
||||
ipn,
|
||||
onDone,
|
||||
}: {
|
||||
def: SSHSessionDef
|
||||
ipn: IPN
|
||||
onDone: () => void
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
runSSHSession(ref.current, def, ipn, onDone)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return <div class="flex-grow bg-black p-2 overflow-hidden" ref={ref} />
|
||||
}
|
||||
|
||||
function NoSSHPeers() {
|
||||
return (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
None of your machines have
|
||||
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
|
||||
Tailscale SSH
|
||||
</a>
|
||||
enabled. Give it a try!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SSHForm({
|
||||
sshPeers,
|
||||
onSubmit,
|
||||
}: {
|
||||
sshPeers: IPNNetMapPeerNode[]
|
||||
onSubmit: (def: SSHFormSessionDef) => void
|
||||
}) {
|
||||
sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
const [username, setUsername] = useState("")
|
||||
const [hostname, setHostname] = useState(sshPeers[0].name)
|
||||
return (
|
||||
<form
|
||||
class="container mx-auto px-4 flex justify-center"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onSubmit({ username, hostname })
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input username"
|
||||
placeholder="Username"
|
||||
onChange={(e) => setUsername(e.currentTarget.value)}
|
||||
/>
|
||||
<div class="select-with-arrow mx-2">
|
||||
<select
|
||||
class="select"
|
||||
onChange={(e) => setHostname(e.currentTarget.value)}
|
||||
>
|
||||
{sshPeers.map((p) => (
|
||||
<option key={p.nodeKey}>{p.name.split(".")[0]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
|
||||
value="SSH"
|
||||
onClick={(e) => {
|
||||
if (e.altKey) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onSubmit({ username, hostname, newWindow: true })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const NewWindow = ({
|
||||
children,
|
||||
close,
|
||||
}: {
|
||||
children: VNode
|
||||
close: () => void
|
||||
}) => {
|
||||
const newWindow = useMemo(() => {
|
||||
const newWindow = window.open(undefined, undefined, "width=600,height=400")
|
||||
if (newWindow) {
|
||||
const containerNode = newWindow.document.createElement("div")
|
||||
containerNode.className = "h-screen flex flex-col overflow-hidden"
|
||||
newWindow.document.body.appendChild(containerNode)
|
||||
|
||||
for (const linkNode of document.querySelectorAll(
|
||||
"head link[rel=stylesheet]"
|
||||
)) {
|
||||
const newLink = document.createElement("link")
|
||||
newLink.rel = "stylesheet"
|
||||
newLink.href = (linkNode as HTMLLinkElement).href
|
||||
newWindow.document.head.appendChild(newLink)
|
||||
}
|
||||
}
|
||||
return newWindow
|
||||
}, [])
|
||||
if (!newWindow) {
|
||||
console.error("Could not open window")
|
||||
return null
|
||||
}
|
||||
newWindow.onbeforeunload = () => {
|
||||
close()
|
||||
}
|
||||
|
||||
useEffect(() => () => newWindow.close(), [])
|
||||
return createPortal(children, newWindow.document.body.lastChild as Element)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import { useState } from "preact/hooks"
|
||||
import * as qrcode from "qrcode"
|
||||
|
||||
export function URLDisplay({ url }: { url: string }) {
|
||||
const [dataURL, setDataURL] = useState("")
|
||||
qrcode.toDataURL(url, { width: 512 }, (err, dataURL) => {
|
||||
if (err) {
|
||||
console.error("Error generating QR code", err)
|
||||
} else {
|
||||
setDataURL(dataURL)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center justify-items-center">
|
||||
<a href={url} class="link" target="_blank">
|
||||
<img
|
||||
src={dataURL}
|
||||
class="mx-auto"
|
||||
width="256"
|
||||
height="256"
|
||||
alt="QR Code of URL"
|
||||
/>
|
||||
{url}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -73,3 +73,7 @@
|
||||
background-color: currentColor;
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
}
|
||||
|
||||
body.ssh-active #ssh-form {
|
||||
@apply hidden;
|
||||
}
|
||||
58
cmd/tsconnect/src/index.ts
Normal file
58
cmd/tsconnect/src/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import "./wasm_exec"
|
||||
import wasmUrl from "./main.wasm"
|
||||
import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier"
|
||||
import { sessionStateStorage } from "./js-state-store"
|
||||
|
||||
const go = new Go()
|
||||
WebAssembly.instantiateStreaming(
|
||||
fetch(`./dist/${wasmUrl}`),
|
||||
go.importObject
|
||||
).then((result) => {
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
go.run(result.instance).then(() => handleGoPanic())
|
||||
const ipn = newIPN({
|
||||
// Persist IPN state in sessionStorage in development, so that we don't need
|
||||
// to re-authorize every time we reload the page.
|
||||
stateStorage: DEBUG ? sessionStateStorage : undefined,
|
||||
})
|
||||
ipn.run({
|
||||
notifyState: notifyState.bind(null, ipn),
|
||||
notifyNetMap: notifyNetMap.bind(null, ipn),
|
||||
notifyBrowseToURL: notifyBrowseToURL.bind(null, ipn),
|
||||
notifyPanicRecover: handleGoPanic,
|
||||
})
|
||||
})
|
||||
|
||||
function handleGoPanic(err?: string) {
|
||||
if (DEBUG && err) {
|
||||
console.error("Go panic", err)
|
||||
}
|
||||
if (panicNode) {
|
||||
panicNode.remove()
|
||||
}
|
||||
panicNode = document.createElement("div")
|
||||
panicNode.className =
|
||||
"rounded bg-red-500 p-2 absolute top-2 right-2 text-white font-bold text-right cursor-pointer"
|
||||
panicNode.textContent = "Tailscale has encountered an error."
|
||||
const panicDetailNode = document.createElement("div")
|
||||
panicDetailNode.className = "text-sm font-normal"
|
||||
panicDetailNode.textContent = "Click to reload"
|
||||
panicNode.appendChild(panicDetailNode)
|
||||
panicNode.addEventListener("click", () => location.reload(), {
|
||||
once: true,
|
||||
})
|
||||
document.body.appendChild(panicNode)
|
||||
setTimeout(() => {
|
||||
panicNode!.remove()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
let panicNode: HTMLDivElement | undefined
|
||||
|
||||
export function getContentNode(): HTMLDivElement {
|
||||
return document.querySelector("#content") as HTMLDivElement
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Terminal, ITerminalOptions } from "xterm"
|
||||
import { FitAddon } from "xterm-addon-fit"
|
||||
import { WebLinksAddon } from "xterm-addon-web-links"
|
||||
|
||||
export type SSHSessionDef = {
|
||||
username: string
|
||||
hostname: string
|
||||
}
|
||||
|
||||
export function runSSHSession(
|
||||
termContainerNode: HTMLDivElement,
|
||||
def: SSHSessionDef,
|
||||
ipn: IPN,
|
||||
onDone: () => void,
|
||||
terminalOptions?: ITerminalOptions
|
||||
) {
|
||||
const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
allowProposedApi: true,
|
||||
...terminalOptions,
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
term.open(termContainerNode)
|
||||
fitAddon.fit()
|
||||
|
||||
const webLinksAddon = new WebLinksAddon((event, uri) =>
|
||||
event.view?.open(uri, "_blank", "noopener")
|
||||
)
|
||||
term.loadAddon(webLinksAddon)
|
||||
|
||||
let onDataHook: ((data: string) => void) | undefined
|
||||
term.onData((e) => {
|
||||
onDataHook?.(e)
|
||||
})
|
||||
|
||||
term.focus()
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let handleBeforeUnload: ((e: BeforeUnloadEvent) => void) | undefined
|
||||
|
||||
const sshSession = ipn.ssh(def.hostname, def.username, {
|
||||
writeFn(input) {
|
||||
term.write(input)
|
||||
},
|
||||
writeErrorFn(err) {
|
||||
console.error(err)
|
||||
term.write(err)
|
||||
},
|
||||
setReadFn(hook) {
|
||||
onDataHook = hook
|
||||
},
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
onDone() {
|
||||
resizeObserver?.disconnect()
|
||||
term.dispose()
|
||||
if (handleBeforeUnload) {
|
||||
parentWindow.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
}
|
||||
onDone()
|
||||
},
|
||||
})
|
||||
|
||||
// Make terminal and SSH session track the size of the containing DOM node.
|
||||
resizeObserver = new parentWindow.ResizeObserver(() => fitAddon.fit())
|
||||
resizeObserver.observe(termContainerNode)
|
||||
term.onResize(({ rows, cols }) => sshSession.resize(rows, cols))
|
||||
|
||||
// Close the session if the user closes the window without an explicit
|
||||
// exit.
|
||||
handleBeforeUnload = () => sshSession.close()
|
||||
parentWindow.addEventListener("beforeunload", handleBeforeUnload)
|
||||
}
|
||||
74
cmd/tsconnect/src/login.ts
Normal file
74
cmd/tsconnect/src/login.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import * as qrcode from "qrcode"
|
||||
import { getContentNode } from "./index"
|
||||
|
||||
export async function showLoginURL(url: string) {
|
||||
if (loginNode) {
|
||||
loginNode.remove()
|
||||
}
|
||||
loginNode = document.createElement("div")
|
||||
loginNode.className = "flex flex-col items-center justify-items-center"
|
||||
const linkNode = document.createElement("a")
|
||||
linkNode.className = "link"
|
||||
linkNode.href = url
|
||||
linkNode.target = "_blank"
|
||||
loginNode.appendChild(linkNode)
|
||||
|
||||
try {
|
||||
const dataURL = await qrcode.toDataURL(url, { width: 512 })
|
||||
const imageNode = document.createElement("img")
|
||||
imageNode.className = "mx-auto"
|
||||
imageNode.src = dataURL
|
||||
imageNode.width = 256
|
||||
imageNode.height = 256
|
||||
linkNode.appendChild(imageNode)
|
||||
} catch (err) {
|
||||
console.error("Could not generate QR code:", err)
|
||||
}
|
||||
|
||||
linkNode.appendChild(document.createTextNode(url))
|
||||
|
||||
getContentNode().appendChild(loginNode)
|
||||
}
|
||||
|
||||
export function hideLoginURL() {
|
||||
if (!loginNode) {
|
||||
return
|
||||
}
|
||||
loginNode.remove()
|
||||
loginNode = undefined
|
||||
}
|
||||
|
||||
let loginNode: HTMLDivElement | undefined
|
||||
|
||||
export function showLogoutButton(ipn: IPN) {
|
||||
if (logoutButtonNode) {
|
||||
logoutButtonNode.remove()
|
||||
}
|
||||
logoutButtonNode = document.createElement("button")
|
||||
logoutButtonNode.className =
|
||||
"button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
|
||||
logoutButtonNode.textContent = "Logout"
|
||||
logoutButtonNode.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
ipn.logout()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
const headerNode = document.getElementsByTagName("header")[0]!
|
||||
headerNode.appendChild(logoutButtonNode)
|
||||
}
|
||||
|
||||
export function hideLogoutButton() {
|
||||
if (!logoutButtonNode) {
|
||||
return
|
||||
}
|
||||
logoutButtonNode.remove()
|
||||
logoutButtonNode = undefined
|
||||
}
|
||||
|
||||
let logoutButtonNode: HTMLButtonElement | undefined
|
||||
65
cmd/tsconnect/src/notifier.ts
Normal file
65
cmd/tsconnect/src/notifier.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import {
|
||||
showLoginURL,
|
||||
hideLoginURL,
|
||||
showLogoutButton,
|
||||
hideLogoutButton,
|
||||
} from "./login"
|
||||
import { showSSHForm, hideSSHForm } from "./ssh"
|
||||
import { IPNState } from "./wasm_js"
|
||||
|
||||
/**
|
||||
* @fileoverview Notification callback functions (bridged from ipn.Notify)
|
||||
*/
|
||||
|
||||
export function notifyState(ipn: IPN, state: IPNState) {
|
||||
let stateLabel
|
||||
switch (state) {
|
||||
case IPNState.NoState:
|
||||
stateLabel = "Initializing…"
|
||||
break
|
||||
case IPNState.InUseOtherUser:
|
||||
stateLabel = "In-use by another user"
|
||||
break
|
||||
case IPNState.NeedsLogin:
|
||||
stateLabel = "Needs Login"
|
||||
hideLogoutButton()
|
||||
hideSSHForm()
|
||||
ipn.login()
|
||||
break
|
||||
case IPNState.NeedsMachineAuth:
|
||||
stateLabel = "Needs authorization"
|
||||
break
|
||||
case IPNState.Stopped:
|
||||
stateLabel = "Stopped"
|
||||
hideLogoutButton()
|
||||
hideSSHForm()
|
||||
break
|
||||
case IPNState.Starting:
|
||||
stateLabel = "Starting…"
|
||||
break
|
||||
case IPNState.Running:
|
||||
stateLabel = "Running"
|
||||
hideLoginURL()
|
||||
showLogoutButton(ipn)
|
||||
break
|
||||
}
|
||||
const stateNode = document.querySelector("#state") as HTMLDivElement
|
||||
stateNode.textContent = stateLabel ?? ""
|
||||
}
|
||||
|
||||
export function notifyNetMap(ipn: IPN, netMapStr: string) {
|
||||
const netMap = JSON.parse(netMapStr) as IPNNetMap
|
||||
if (DEBUG) {
|
||||
console.log("Received net map: " + JSON.stringify(netMap, null, 2))
|
||||
}
|
||||
|
||||
showSSHForm(netMap.peers, ipn)
|
||||
}
|
||||
|
||||
export function notifyBrowseToURL(ipn: IPN, url: string) {
|
||||
showLoginURL(url)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/* Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. */
|
||||
/* Use of this source code is governed by a BSD-style */
|
||||
/* license that can be found in the LICENSE file. */
|
||||
|
||||
@import "xterm/css/xterm.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Type definitions need to be manually imported for dts-bundle-generator to
|
||||
// discover them.
|
||||
/// <reference path="../types/esbuild.d.ts" />
|
||||
/// <reference path="../types/wasm_js.d.ts" />
|
||||
|
||||
import "../wasm_exec"
|
||||
import wasmURL from "./main.wasm"
|
||||
|
||||
/**
|
||||
* Superset of the IPNConfig type, with additional configuration that is
|
||||
* needed for the package to function.
|
||||
*/
|
||||
type IPNPackageConfig = IPNConfig & {
|
||||
// Auth key used to intitialize the Tailscale client (required)
|
||||
authKey: string
|
||||
// URL of the main.wasm file that is included in the page, if it is not
|
||||
// accessible via a relative URL.
|
||||
wasmURL?: string
|
||||
// Funtion invoked if the Go process panics or unexpectedly exits.
|
||||
panicHandler: (err: string) => void
|
||||
}
|
||||
|
||||
export async function createIPN(config: IPNPackageConfig): Promise<IPN> {
|
||||
const go = new Go()
|
||||
const wasmInstance = await WebAssembly.instantiateStreaming(
|
||||
fetch(config.wasmURL ?? wasmURL),
|
||||
go.importObject
|
||||
)
|
||||
// The Go process should never exit, if it does then it's an unhandled panic.
|
||||
go.run(wasmInstance.instance).then(() =>
|
||||
config.panicHandler("Unexpected shutdown")
|
||||
)
|
||||
|
||||
return newIPN(config)
|
||||
}
|
||||
|
||||
export { runSSHSession } from "../lib/ssh"
|
||||
98
cmd/tsconnect/src/ssh.ts
Normal file
98
cmd/tsconnect/src/ssh.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import { Terminal } from "xterm"
|
||||
import { FitAddon } from "xterm-addon-fit"
|
||||
import { getContentNode } from "./index"
|
||||
|
||||
export function showSSHForm(peers: IPNNetMapPeerNode[], ipn: IPN) {
|
||||
const formNode = document.querySelector("#ssh-form") as HTMLDivElement
|
||||
const noSSHNode = document.querySelector("#no-ssh") as HTMLDivElement
|
||||
|
||||
const sshPeers = peers.filter(
|
||||
(p) => p.tailscaleSSHEnabled && p.online !== false
|
||||
)
|
||||
if (sshPeers.length == 0) {
|
||||
formNode.classList.add("hidden")
|
||||
noSSHNode.classList.remove("hidden")
|
||||
return
|
||||
}
|
||||
sshPeers.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const selectNode = formNode.querySelector("select")!
|
||||
selectNode.innerHTML = ""
|
||||
for (const p of sshPeers) {
|
||||
const option = document.createElement("option")
|
||||
option.textContent = p.name.split(".")[0]
|
||||
option.value = p.name
|
||||
selectNode.appendChild(option)
|
||||
}
|
||||
|
||||
const usernameNode = formNode.querySelector(".username") as HTMLInputElement
|
||||
formNode.onsubmit = (e) => {
|
||||
e.preventDefault()
|
||||
const hostname = selectNode.value
|
||||
ssh(hostname, usernameNode.value, ipn)
|
||||
}
|
||||
|
||||
noSSHNode.classList.add("hidden")
|
||||
formNode.classList.remove("hidden")
|
||||
}
|
||||
|
||||
export function hideSSHForm() {
|
||||
const formNode = document.querySelector("#ssh-form") as HTMLDivElement
|
||||
formNode.classList.add("hidden")
|
||||
}
|
||||
|
||||
function ssh(hostname: string, username: string, ipn: IPN) {
|
||||
document.body.classList.add("ssh-active")
|
||||
const termContainerNode = document.createElement("div")
|
||||
termContainerNode.className = "flex-grow bg-black p-2 overflow-hidden"
|
||||
getContentNode().appendChild(termContainerNode)
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
})
|
||||
const fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
term.open(termContainerNode)
|
||||
fitAddon.fit()
|
||||
|
||||
let onDataHook: ((data: string) => void) | undefined
|
||||
term.onData((e) => {
|
||||
onDataHook?.(e)
|
||||
})
|
||||
|
||||
term.focus()
|
||||
|
||||
const sshSession = ipn.ssh(hostname, username, {
|
||||
writeFn: (input) => term.write(input),
|
||||
setReadFn: (hook) => (onDataHook = hook),
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
onDone: () => {
|
||||
resizeObserver.disconnect()
|
||||
term.dispose()
|
||||
termContainerNode.remove()
|
||||
document.body.classList.remove("ssh-active")
|
||||
window.removeEventListener("beforeunload", beforeUnloadListener)
|
||||
},
|
||||
})
|
||||
|
||||
// Make terminal and SSH session track the size of the containing DOM node.
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
resizeObserver.observe(termContainerNode)
|
||||
term.onResize(({ rows, cols }) => {
|
||||
sshSession.resize(rows, cols)
|
||||
})
|
||||
|
||||
// Close the session if the user closes the window without an explicit
|
||||
// exit.
|
||||
const beforeUnloadListener = () => {
|
||||
sshSession.close()
|
||||
}
|
||||
window.addEventListener("beforeunload", beforeUnloadListener)
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
/**
|
||||
* @fileoverview Type definitions for types exported by the wasm_js.go Go
|
||||
* module.
|
||||
* module. Not actually a .d.ts file so that we can use enums from it in
|
||||
* esbuild's simplified TypeScript compiler (see https://github.com/evanw/esbuild/issues/2298#issuecomment-1146378367)
|
||||
*/
|
||||
|
||||
declare global {
|
||||
@@ -19,14 +20,15 @@ declare global {
|
||||
username: string,
|
||||
termConfig: {
|
||||
writeFn: (data: string) => void
|
||||
writeErrorFn: (err: string) => void
|
||||
setReadFn: (readFn: (data: string) => void) => void
|
||||
rows: number
|
||||
cols: number
|
||||
onDone: () => void
|
||||
}
|
||||
): IPNSSHSession
|
||||
fetch(url: string): Promise<{
|
||||
fetch(
|
||||
url: string
|
||||
): Promise<{
|
||||
status: number
|
||||
statusText: string
|
||||
text: () => Promise<string>
|
||||
@@ -45,9 +47,6 @@ declare global {
|
||||
|
||||
type IPNConfig = {
|
||||
stateStorage?: IPNStateStorage
|
||||
authKey?: string
|
||||
controlURL?: string
|
||||
hostname?: string
|
||||
}
|
||||
|
||||
type IPNCallbacks = {
|
||||
@@ -77,23 +76,23 @@ declare global {
|
||||
online?: boolean
|
||||
tailscaleSSHEnabled: boolean
|
||||
}
|
||||
|
||||
/** Mirrors values from ipn/backend.go */
|
||||
type IPNState =
|
||||
| "NoState"
|
||||
| "InUseOtherUser"
|
||||
| "NeedsLogin"
|
||||
| "NeedsMachineAuth"
|
||||
| "Stopped"
|
||||
| "Starting"
|
||||
| "Running"
|
||||
|
||||
/** Mirrors values from MachineStatus in tailcfg.go */
|
||||
type IPNMachineStatus =
|
||||
| "MachineUnknown"
|
||||
| "MachineUnauthorized"
|
||||
| "MachineAuthorized"
|
||||
| "MachineInvalid"
|
||||
}
|
||||
|
||||
export {}
|
||||
/** Mirrors values from ipn/backend.go */
|
||||
export const enum IPNState {
|
||||
NoState = 0,
|
||||
InUseOtherUser = 1,
|
||||
NeedsLogin = 2,
|
||||
NeedsMachineAuth = 3,
|
||||
Stopped = 4,
|
||||
Starting = 5,
|
||||
Running = 6,
|
||||
}
|
||||
|
||||
/** Mirrors values from MachineStatus in tailcfg.go */
|
||||
export const enum IPNMachineStatus {
|
||||
MachineUnknown = 0,
|
||||
MachineUnauthorized = 1,
|
||||
MachineAuthorized = 2,
|
||||
MachineInvalid = 3,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.ts", "./src/**/*.tsx"],
|
||||
content: ["./index.html", "./src/**/*.ts"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -20,10 +20,8 @@ import (
|
||||
var (
|
||||
addr = flag.String("addr", ":9090", "address to listen on")
|
||||
distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
|
||||
pkgDir = flag.String("pkgdir", "./pkg", "path of directory to place NPM package build output in")
|
||||
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
|
||||
fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
|
||||
devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -36,12 +34,8 @@ func main() {
|
||||
switch flag.Arg(0) {
|
||||
case "dev":
|
||||
runDev()
|
||||
case "dev-pkg":
|
||||
runDevPkg()
|
||||
case "build":
|
||||
runBuild()
|
||||
case "build-pkg":
|
||||
runBuildPkg()
|
||||
case "serve":
|
||||
runServe()
|
||||
default:
|
||||
|
||||
@@ -31,20 +31,16 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/words"
|
||||
)
|
||||
|
||||
// ControlURL defines the URL to be used for connection to Control.
|
||||
var ControlURL = ipn.DefaultControlURL
|
||||
|
||||
func main() {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 1 {
|
||||
@@ -60,41 +56,7 @@ func main() {
|
||||
|
||||
func newIPN(jsConfig js.Value) map[string]any {
|
||||
netns.SetEnabled(false)
|
||||
|
||||
var store ipn.StateStore
|
||||
if jsStateStorage := jsConfig.Get("stateStorage"); !jsStateStorage.IsUndefined() {
|
||||
store = &jsStateStore{jsStateStorage}
|
||||
} else {
|
||||
store = new(mem.Store)
|
||||
}
|
||||
|
||||
controlURL := ControlURL
|
||||
if jsControlURL := jsConfig.Get("controlURL"); jsControlURL.Type() == js.TypeString {
|
||||
controlURL = jsControlURL.String()
|
||||
}
|
||||
|
||||
var authKey string
|
||||
if jsAuthKey := jsConfig.Get("authKey"); jsAuthKey.Type() == js.TypeString {
|
||||
authKey = jsAuthKey.String()
|
||||
}
|
||||
|
||||
var hostname string
|
||||
if jsHostname := jsConfig.Get("hostname"); jsHostname.Type() == js.TypeString {
|
||||
hostname = jsHostname.String()
|
||||
} else {
|
||||
hostname = generateHostname()
|
||||
}
|
||||
|
||||
lpc := getOrCreateLogPolicyConfig(store)
|
||||
c := logtail.Config{
|
||||
Collection: lpc.Collection,
|
||||
PrivateID: lpc.PrivateID,
|
||||
// NewZstdEncoder is intentionally not passed in, compressed requests
|
||||
// set HTTP headers that are not supported by the no-cors fetching mode.
|
||||
HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
|
||||
}
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
logf := logtail.Logf
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
dialer := new(tsdial.Dialer)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
@@ -124,7 +86,14 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, nil, ipnserver.Options{
|
||||
jsStateStorage := jsConfig.Get("stateStorage")
|
||||
var store ipn.StateStore
|
||||
if jsStateStorage.IsUndefined() {
|
||||
store = new(mem.Store)
|
||||
} else {
|
||||
store = &jsStateStore{jsStateStorage}
|
||||
}
|
||||
srv, err := ipnserver.New(log.Printf, "some-logid", store, eng, dialer, nil, ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
LoginFlags: controlclient.LoginEphemeral,
|
||||
})
|
||||
@@ -135,12 +104,9 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
ns.SetLocalBackend(lb)
|
||||
|
||||
jsIPN := &jsIPN{
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
controlURL: controlURL,
|
||||
authKey: authKey,
|
||||
hostname: hostname,
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
@@ -196,34 +162,14 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
}
|
||||
|
||||
type jsIPN struct {
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
controlURL string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
var jsIPNState = map[ipn.State]string{
|
||||
ipn.NoState: "NoState",
|
||||
ipn.InUseOtherUser: "InUseOtherUser",
|
||||
ipn.NeedsLogin: "NeedsLogin",
|
||||
ipn.NeedsMachineAuth: "NeedsMachineAuth",
|
||||
ipn.Stopped: "Stopped",
|
||||
ipn.Starting: "Starting",
|
||||
ipn.Running: "Running",
|
||||
}
|
||||
|
||||
var jsMachineStatus = map[tailcfg.MachineStatus]string{
|
||||
tailcfg.MachineUnknown: "MachineUnknown",
|
||||
tailcfg.MachineUnauthorized: "MachineUnauthorized",
|
||||
tailcfg.MachineAuthorized: "MachineAuthorized",
|
||||
tailcfg.MachineInvalid: "MachineInvalid",
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
notifyState := func(state ipn.State) {
|
||||
jsCallbacks.Call("notifyState", jsIPNState[state])
|
||||
jsCallbacks.Call("notifyState", int(state))
|
||||
}
|
||||
notifyState(ipn.NoState)
|
||||
|
||||
@@ -242,7 +188,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
if n.State != nil {
|
||||
notifyState(*n.State)
|
||||
}
|
||||
if nm := n.NetMap; nm != nil {
|
||||
if nm := n.NetMap; nm != nil && i.lb.State() == ipn.Running {
|
||||
jsNetMap := jsNetMap{
|
||||
Self: jsNetMapSelfNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
@@ -251,7 +197,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
NodeKey: nm.NodeKey.String(),
|
||||
MachineKey: nm.MachineKey.String(),
|
||||
},
|
||||
MachineStatus: jsMachineStatus[nm.MachineStatus],
|
||||
MachineStatus: int(nm.MachineStatus),
|
||||
},
|
||||
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
|
||||
name := p.Name
|
||||
@@ -286,13 +232,12 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
err := i.lb.Start(ipn.Options{
|
||||
StateKey: "wasm",
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: i.controlURL,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: i.hostname,
|
||||
Hostname: generateHostname(),
|
||||
},
|
||||
AuthKey: i.authKey,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Start error: %v", err)
|
||||
@@ -349,22 +294,21 @@ type jsSSHSession struct {
|
||||
username string
|
||||
termConfig js.Value
|
||||
session *ssh.Session
|
||||
|
||||
pendingResizeRows int
|
||||
pendingResizeCols int
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) Run() {
|
||||
writeFn := s.termConfig.Get("writeFn")
|
||||
writeErrorFn := s.termConfig.Get("writeErrorFn")
|
||||
setReadFn := s.termConfig.Get("setReadFn")
|
||||
rows := s.termConfig.Get("rows").Int()
|
||||
cols := s.termConfig.Get("cols").Int()
|
||||
onDone := s.termConfig.Get("onDone")
|
||||
defer onDone.Invoke()
|
||||
|
||||
write := func(s string) {
|
||||
writeFn.Invoke(s)
|
||||
}
|
||||
writeError := func(label string, err error) {
|
||||
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
write(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -387,6 +331,7 @@ func (s *jsSSHSession) Run() {
|
||||
return
|
||||
}
|
||||
defer sshConn.Close()
|
||||
write("SSH Connected\r\n")
|
||||
|
||||
sshClient := ssh.NewClient(sshConn, nil, nil)
|
||||
defer sshClient.Close()
|
||||
@@ -397,6 +342,7 @@ func (s *jsSSHSession) Run() {
|
||||
return
|
||||
}
|
||||
s.session = session
|
||||
write("Session Established\r\n")
|
||||
defer session.Close()
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
@@ -417,14 +363,6 @@ func (s *jsSSHSession) Run() {
|
||||
return nil
|
||||
}))
|
||||
|
||||
// We might have gotten a resize notification since we started opening the
|
||||
// session, pick up the latest size.
|
||||
if s.pendingResizeRows != 0 {
|
||||
rows = s.pendingResizeRows
|
||||
}
|
||||
if s.pendingResizeCols != 0 {
|
||||
cols = s.pendingResizeCols
|
||||
}
|
||||
err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
|
||||
|
||||
if err != nil {
|
||||
@@ -450,11 +388,6 @@ func (s *jsSSHSession) Close() error {
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) Resize(rows, cols int) error {
|
||||
if s.session == nil {
|
||||
s.pendingResizeRows = rows
|
||||
s.pendingResizeCols = cols
|
||||
return nil
|
||||
}
|
||||
return s.session.WindowChange(rows, cols)
|
||||
}
|
||||
|
||||
@@ -512,7 +445,7 @@ type jsNetMapNode struct {
|
||||
|
||||
type jsNetMapSelfNode struct {
|
||||
jsNetMapNode
|
||||
MachineStatus string `json:"machineStatus"`
|
||||
MachineStatus int `json:"machineStatus"`
|
||||
}
|
||||
|
||||
type jsNetMapPeerNode struct {
|
||||
@@ -594,40 +527,3 @@ func makePromise(f func() (any, error)) js.Value {
|
||||
promiseConstructor := js.Global().Get("Promise")
|
||||
return promiseConstructor.New(handler)
|
||||
}
|
||||
|
||||
const logPolicyStateKey = "log-policy"
|
||||
|
||||
func getOrCreateLogPolicyConfig(state ipn.StateStore) *logpolicy.Config {
|
||||
if configBytes, err := state.ReadState(logPolicyStateKey); err == nil {
|
||||
if config, err := logpolicy.ConfigFromBytes(configBytes); err == nil {
|
||||
return config
|
||||
} else {
|
||||
log.Printf("Could not parse log policy config: %v", err)
|
||||
}
|
||||
} else if err != ipn.ErrStateNotExist {
|
||||
log.Printf("Could not get log policy config from state store: %v", err)
|
||||
}
|
||||
config := logpolicy.NewConfig(logtail.CollectionNode)
|
||||
if err := state.WriteState(logPolicyStateKey, config.ToBytes()); err != nil {
|
||||
log.Printf("Could not save log policy config to state store: %v", err)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// noCORSTransport wraps a RoundTripper and forces the no-cors mode on requests,
|
||||
// so that we can use it with non-CORS-aware servers.
|
||||
type noCORSTransport struct {
|
||||
http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *noCORSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("js.fetch:mode", "no-cors")
|
||||
resp, err := t.RoundTripper.RoundTrip(req)
|
||||
if err == nil {
|
||||
// In no-cors mode no response properties are returned. Populate just
|
||||
// the status so that callers do not think this was an error.
|
||||
resp.StatusCode = http.StatusOK
|
||||
resp.Status = http.StatusText(http.StatusOK)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
@@ -130,15 +130,6 @@ cliui@^6.0.0:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
@@ -190,14 +181,6 @@ dlv@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
|
||||
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
|
||||
|
||||
dts-bundle-generator@^6.12.0:
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/dts-bundle-generator/-/dts-bundle-generator-6.12.0.tgz#0a221bdce5fdd309a56c8556e645f16ed87ab07d"
|
||||
integrity sha512-k/QAvuVaLIdyWRUHduDrWBe4j8PcE6TDt06+f32KHbW7/SmUPbX1O23fFtQgKwUyTBkbIjJFOFtNrF97tJcKug==
|
||||
dependencies:
|
||||
typescript ">=3.0.1"
|
||||
yargs "^17.2.1"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
@@ -208,11 +191,6 @@ encode-utf8@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
|
||||
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
fast-glob@^3.2.11:
|
||||
version "3.2.11"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
|
||||
@@ -256,7 +234,7 @@ function-bind@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
get-caller-file@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
@@ -465,11 +443,6 @@ postcss@^8.4.14:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
preact@^10.10.0:
|
||||
version "10.10.0"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.10.0.tgz#7434750a24b59dae1957d95dc0aa47a4a8e9a180"
|
||||
integrity sha512-fszkg1iJJjq68I4lI8ZsmBiaoQiQHbxf1lNq+72EmC/mZOsFF5zn3k1yv9QGoFgIXzgsdSKtYymLJsrJPoamjQ==
|
||||
|
||||
qrcode@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
|
||||
@@ -545,7 +518,7 @@ source-map-js@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -601,7 +574,7 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
typescript@>=3.0.1, typescript@^4.7.4:
|
||||
typescript@^4.7.4:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
@@ -625,15 +598,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
xtend@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
@@ -644,26 +608,16 @@ xterm-addon-fit@^0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
|
||||
|
||||
xterm@5.0.0-beta.58:
|
||||
version "5.0.0-beta.58"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.58.tgz#e3e96ab9fd24d006ec16cc9351a060cc79e67e80"
|
||||
integrity sha512-gjg39oKdgUKful27+7I1hvSK51lu/LRhdimFhfZyMvdk0iATH0FAfzv1eAvBKWY2UBgYUfxhicTkanYioANdMw==
|
||||
|
||||
xterm-addon-web-links@0.7.0-beta.6:
|
||||
version "0.7.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0-beta.6.tgz#ec63b681b4f0f0135fa039f53664f65fe9d9f43a"
|
||||
integrity sha512-nD/r/GchGTN4c9gAIVLWVoxExTzAUV7E9xZnwsvhuwI4CEE6yqO15ns8g2hdcUrsPyCbNEw05mIrkF6W5Yj8qA==
|
||||
xterm@^4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
|
||||
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yaml@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
@@ -677,11 +631,6 @@ yargs-parser@^18.1.2:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^21.0.0:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^15.3.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
@@ -698,16 +647,3 @@ yargs@^15.3.1:
|
||||
which-module "^2.0.0"
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
|
||||
yargs@^17.2.1:
|
||||
version "17.5.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
|
||||
integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone --clone-only-type=OnlyGetClone
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
|
||||
|
||||
type StructWithoutPtrs struct {
|
||||
Int int
|
||||
@@ -58,7 +58,3 @@ type StructWithSlices struct {
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type OnlyGetClone struct {
|
||||
SinViewerPorFavor bool
|
||||
}
|
||||
|
||||
@@ -196,19 +196,3 @@ var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of OnlyGetClone.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *OnlyGetClone) Clone() *OnlyGetClone {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(OnlyGetClone)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _OnlyGetCloneCloneNeedsRegeneration = OnlyGetClone(struct {
|
||||
SinViewerPorFavor bool
|
||||
}{})
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
|
||||
|
||||
// View returns a readonly view of StructWithPtrs.
|
||||
func (p *StructWithPtrs) View() StructWithPtrsView {
|
||||
|
||||
@@ -327,8 +327,6 @@ var (
|
||||
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
||||
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
|
||||
flagCloneOnlyTypes = flag.String("clone-only-type", "", "comma-separated list of types (a subset of --type) that should only generate a go:generate clone line and not actual views")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -355,18 +353,10 @@ func main() {
|
||||
}
|
||||
it := codegen.NewImportTracker(pkg.Types)
|
||||
|
||||
cloneOnlyType := map[string]bool{}
|
||||
for _, t := range strings.Split(*flagCloneOnlyTypes, ",") {
|
||||
cloneOnlyType[t] = true
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "//go:generate go run tailscale.com/cmd/cloner %s\n\n", strings.Join(flagArgs, " "))
|
||||
runCloner := false
|
||||
for _, typeName := range typeNames {
|
||||
if cloneOnlyType[typeName] {
|
||||
continue
|
||||
}
|
||||
typ, ok := namedTypes[typeName]
|
||||
if !ok {
|
||||
log.Fatalf("could not find type %s", typeName)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
@@ -14,9 +13,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -41,7 +41,6 @@ import (
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -74,7 +73,6 @@ type Direct struct {
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
@@ -110,7 +108,6 @@ type Options struct {
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
|
||||
// GetNLPublicKey specifies an optional function to use
|
||||
// Network Lock. If nil, it's not used.
|
||||
@@ -213,7 +210,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
@@ -489,7 +485,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq sign error: %v", err)
|
||||
}
|
||||
}
|
||||
if debugRegister() {
|
||||
if debugRegister {
|
||||
j, _ := json.MarshalIndent(request, "", "\t")
|
||||
c.logf("RegisterRequest: %s", j)
|
||||
}
|
||||
@@ -522,7 +518,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
return regen, opt.URL, fmt.Errorf("register request: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
@@ -532,7 +528,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
if debugRegister() {
|
||||
if debugRegister {
|
||||
j, _ := json.MarshalIndent(resp, "", "\t")
|
||||
c.logf("RegisterResponse: %s", j)
|
||||
}
|
||||
@@ -714,7 +710,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if DevKnob.DumpNetMaps() {
|
||||
if Debug.NetMap {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
@@ -803,7 +799,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
}
|
||||
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("initial fetch failed %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
@@ -813,7 +809,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
health.NoteMapRequestHeard(request)
|
||||
|
||||
if cb == nil {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -889,7 +885,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
|
||||
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
|
||||
metricMapResponsePings.Add(1)
|
||||
go c.answerPing(pr)
|
||||
go answerPing(c.logf, c.httpc, pr, c.pinger)
|
||||
}
|
||||
if u := resp.PopBrowserURL; u != "" && u != sess.lastPopBrowserURL {
|
||||
sess.lastPopBrowserURL = u
|
||||
@@ -936,7 +932,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
}
|
||||
if resp.Debug.DisableLogTail {
|
||||
logtail.Disable()
|
||||
envknob.SetNoLogsNoSupport()
|
||||
}
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
@@ -944,6 +939,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
if resp.Debug.GoroutineDumpURL != "" {
|
||||
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 {
|
||||
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil {
|
||||
return err
|
||||
@@ -957,17 +954,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
return errors.New("MapResponse lacked node")
|
||||
}
|
||||
|
||||
if d := nm.Debug; d != nil {
|
||||
controlUseDERPRoute.Store(d.DERPRoute)
|
||||
controlTrimWGConfig.Store(d.TrimWGConfig)
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints() {
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
}
|
||||
}
|
||||
if DevKnob.StripCaps() {
|
||||
if Debug.StripCaps {
|
||||
nm.SelfNode.Capabilities = nil
|
||||
}
|
||||
|
||||
@@ -997,7 +989,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
|
||||
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1011,8 +1003,8 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
|
||||
}
|
||||
|
||||
var (
|
||||
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
|
||||
debugMap = envknob.Bool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
@@ -1050,7 +1042,7 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if debugMap() {
|
||||
if debugMap {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
@@ -1087,7 +1079,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debugMap() {
|
||||
if debugMap {
|
||||
if _, ok := v.(*tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
@@ -1109,7 +1101,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
return nil, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 64<<10))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
@@ -1133,23 +1125,25 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DevKnob contains temporary internal-only debug knobs.
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
var DevKnob = initDevKnob()
|
||||
var Debug = initDebug()
|
||||
|
||||
type devKnobs struct {
|
||||
DumpNetMaps func() bool
|
||||
ForceProxyDNS func() bool
|
||||
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps func() bool // strip all local node's control-provided capabilities
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps bool // strip all local node's control-provided capabilities
|
||||
}
|
||||
|
||||
func initDevKnob() devKnobs {
|
||||
return devKnobs{
|
||||
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
|
||||
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
|
||||
func initDebug() debug {
|
||||
return debug{
|
||||
NetMap: envknob.Bool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
|
||||
Disco: envknob.BoolDefaultTrue("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,20 +1151,29 @@ var clockNow = time.Now
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute syncs.AtomicValue[opt.Bool]
|
||||
controlTrimWGConfig syncs.AtomicValue[opt.Bool]
|
||||
controlUseDERPRoute atomic.Value
|
||||
controlTrimWGConfig atomic.Value
|
||||
)
|
||||
|
||||
func setControlAtomic(dst *atomic.Value, v opt.Bool) {
|
||||
old, ok := dst.Load().(opt.Bool)
|
||||
if !ok || old != v {
|
||||
dst.Store(v)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPRouteFlag reports the last reported value from control for whether
|
||||
// DERP route optimization (Issue 150) should be enabled.
|
||||
func DERPRouteFlag() opt.Bool {
|
||||
return controlUseDERPRoute.Load()
|
||||
v, _ := controlUseDERPRoute.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
return controlTrimWGConfig.Load()
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
@@ -1208,39 +1211,21 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
httpc := c.httpc
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n" && c.noiseConfigured()
|
||||
if useNoise {
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
c.logf("failed to get noise client for ping request: %v", err)
|
||||
return
|
||||
}
|
||||
httpc = nc.Client
|
||||
}
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) {
|
||||
if pr.URL == "" {
|
||||
c.logf("invalid PingRequest with no URL")
|
||||
logf("invalid PingRequest with no URL")
|
||||
return
|
||||
}
|
||||
switch pr.Types {
|
||||
case "":
|
||||
answerHeadPing(c.logf, httpc, pr)
|
||||
return
|
||||
case "c2n":
|
||||
if !useNoise && !envknob.Bool("TS_DEBUG_PERMIT_HTTP_C2N") {
|
||||
c.logf("refusing to answer c2n ping without noise")
|
||||
return
|
||||
}
|
||||
answerC2NPing(c.logf, c.c2nHandler, httpc, pr)
|
||||
if pr.Types == "" {
|
||||
answerHeadPing(logf, c, pr)
|
||||
return
|
||||
}
|
||||
for _, t := range strings.Split(pr.Types, ",") {
|
||||
switch pt := tailcfg.PingType(t); pt {
|
||||
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP, tailcfg.PingPeerAPI:
|
||||
go doPingerPing(c.logf, httpc, pr, c.pinger, pt)
|
||||
go doPingerPing(logf, c, pr, pinger, pt)
|
||||
default:
|
||||
c.logf("unsupported ping request type: %q", t)
|
||||
logf("unsupported ping request type: %q", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1267,54 +1252,6 @@ func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
if c2nHandler == nil {
|
||||
logf("answerC2NPing: c2nHandler not defined")
|
||||
return
|
||||
}
|
||||
hreq, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(pr.Payload)))
|
||||
if err != nil {
|
||||
logf("answerC2NPing: ReadRequest: %v", err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerC2NPing: got c2n request for %v ...", hreq.RequestURI)
|
||||
}
|
||||
handlerTimeout := time.Minute
|
||||
if v := hreq.Header.Get("C2n-Handler-Timeout"); v != "" {
|
||||
handlerTimeout, _ = time.ParseDuration(v)
|
||||
}
|
||||
handlerCtx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
||||
defer cancel()
|
||||
hreq = hreq.WithContext(handlerCtx)
|
||||
rec := httptest.NewRecorder()
|
||||
c2nHandler.ServeHTTP(rec, hreq)
|
||||
cancel()
|
||||
|
||||
c2nResBuf := new(bytes.Buffer)
|
||||
rec.Result().Write(c2nResBuf)
|
||||
|
||||
replyCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(replyCtx, "POST", pr.URL, c2nResBuf)
|
||||
if err != nil {
|
||||
logf("answerC2NPing: NewRequestWithContext: %v", err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerC2NPing: sending POST ping to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
logf("answerC2NPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("answerC2NPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error {
|
||||
const maxSleep = 5 * time.Minute
|
||||
if d > maxSleep {
|
||||
@@ -1391,13 +1328,13 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.serverHost, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
@@ -1463,7 +1400,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -47,7 +46,6 @@ type mapSession struct {
|
||||
lastDomain string
|
||||
lastHealth []string
|
||||
lastPopBrowserURL string
|
||||
stickyDebug tailcfg.Debug // accumulated opt.Bool values
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
@@ -116,28 +114,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
|
||||
debug := resp.Debug
|
||||
if debug != nil {
|
||||
if debug.RandomizeClientPort {
|
||||
debug.SetRandomizeClientPort.Set(true)
|
||||
}
|
||||
if debug.ForceBackgroundSTUN {
|
||||
debug.SetForceBackgroundSTUN.Set(true)
|
||||
}
|
||||
copyDebugOptBools(&ms.stickyDebug, debug)
|
||||
} else if ms.stickyDebug != (tailcfg.Debug{}) {
|
||||
debug = new(tailcfg.Debug)
|
||||
}
|
||||
if debug != nil {
|
||||
copyDebugOptBools(debug, &ms.stickyDebug)
|
||||
if !debug.ForceBackgroundSTUN {
|
||||
debug.ForceBackgroundSTUN, _ = ms.stickyDebug.SetForceBackgroundSTUN.Get()
|
||||
}
|
||||
if !debug.RandomizeClientPort {
|
||||
debug.RandomizeClientPort, _ = ms.stickyDebug.SetRandomizeClientPort.Get()
|
||||
}
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
@@ -150,7 +126,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
Debug: resp.Debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
@@ -190,7 +166,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
if DevKnob.ForceProxyDNS() {
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
ms.netMapBuilding = nil
|
||||
@@ -310,9 +286,6 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if v := ec.Capabilities; v != nil {
|
||||
n.Capabilities = *v
|
||||
}
|
||||
if v := ec.KeySignature; v != nil {
|
||||
n.KeySignature = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,13 +329,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
return v2
|
||||
}
|
||||
|
||||
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
|
||||
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
|
||||
|
||||
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
|
||||
switch {
|
||||
default:
|
||||
return in
|
||||
case debugSelfIPv6Only():
|
||||
case debugSelfIPv6Only:
|
||||
for _, a := range in {
|
||||
if a.Addr().Is6() {
|
||||
ret = append(ret, a)
|
||||
@@ -371,18 +344,3 @@ func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
func copyDebugOptBools(dst, src *tailcfg.Debug) {
|
||||
copy := func(v *opt.Bool, s opt.Bool) {
|
||||
if s != "" {
|
||||
*v = s
|
||||
}
|
||||
}
|
||||
copy(&dst.DERPRoute, src.DERPRoute)
|
||||
copy(&dst.DisableSubnetsIfPAC, src.DisableSubnetsIfPAC)
|
||||
copy(&dst.DisableUPnP, src.DisableUPnP)
|
||||
copy(&dst.OneCGNATRoute, src.OneCGNATRoute)
|
||||
copy(&dst.SetForceBackgroundSTUN, src.SetForceBackgroundSTUN)
|
||||
copy(&dst.SetRandomizeClientPort, src.SetRandomizeClientPort)
|
||||
copy(&dst.TrimWGConfig, src.TrimWGConfig)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
@@ -209,20 +207,6 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
Key: key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "change_key_signature",
|
||||
prev: peers(n(1, "foo")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
KeySignature: []byte{3, 4},
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "foo",
|
||||
KeySignature: []byte{3, 4},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "change_disco_key",
|
||||
prev: peers(n(1, "foo")),
|
||||
@@ -465,152 +449,3 @@ func TestNetmapForResponse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResposnes
|
||||
// entirely or have their opt.Bool values unspecified between MapResponses in a
|
||||
// session and that should mean no change. (as of capver 37). But two Debug
|
||||
// fields existed prior to capver 37 that weren't opt.Bool; we test that we both
|
||||
// still accept the non-opt.Bool form from control for RandomizeClientPort and
|
||||
// ForceBackgroundSTUN and also accept the new form, keeping the old form in
|
||||
// sync.
|
||||
func TestDeltaDebug(t *testing.T) {
|
||||
type step struct {
|
||||
got *tailcfg.Debug
|
||||
want *tailcfg.Debug
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []step
|
||||
}{
|
||||
{
|
||||
name: "nothing-to-nothing",
|
||||
steps: []step{
|
||||
{nil, nil},
|
||||
{nil, nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sticky-with-old-style-randomize-client-port",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{RandomizeClientPort: true},
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
nil, // not sent by server
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sticky-with-new-style-randomize-client-port",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetRandomizeClientPort: "true"},
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
nil, // not sent by server
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opt-bool-sticky-changing-over-time",
|
||||
steps: []step{
|
||||
{nil, nil},
|
||||
{nil, nil},
|
||||
{
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
},
|
||||
{
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy-ForceBackgroundSTUN",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opt-bool-SetForceBackgroundSTUN",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "true"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "server-reset-to-default",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "true"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "unset"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: false, SetForceBackgroundSTUN: "unset"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ms := newTestMapSession(t)
|
||||
for stepi, s := range tt.steps {
|
||||
nm := ms.netmapForResponse(&tailcfg.MapResponse{Debug: s.got})
|
||||
if !reflect.DeepEqual(nm.Debug, s.want) {
|
||||
t.Errorf("unexpected result at step index %v; got: %s", stepi, must.Get(json.Marshal(nm.Debug)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that copyDebugOptBools doesn't missing any opt.Bools.
|
||||
func TestCopyDebugOptBools(t *testing.T) {
|
||||
rt := reflect.TypeOf(tailcfg.Debug{})
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
sf := rt.Field(i)
|
||||
if sf.Type != reflect.TypeOf(opt.Bool("")) {
|
||||
continue
|
||||
}
|
||||
var src, dst tailcfg.Debug
|
||||
reflect.ValueOf(&src).Elem().Field(i).Set(reflect.ValueOf(opt.Bool("true")))
|
||||
if src == (tailcfg.Debug{}) {
|
||||
t.Fatalf("failed to set field %v", sf.Name)
|
||||
}
|
||||
copyDebugOptBools(&dst, &src)
|
||||
if src != dst {
|
||||
t.Fatalf("copyDebugOptBools didn't copy field %v", sf.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user