Compare commits
158 Commits
mihaip/js-
...
gitops-1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b14e31831a | ||
|
|
0b00b7a135 | ||
|
|
70ed22ccf9 | ||
|
|
7ca17b6bdb | ||
|
|
e945d87d76 | ||
|
|
1ac4a26fee | ||
|
|
761163815c | ||
|
|
9f6c8517e0 | ||
|
|
27f36f77c3 | ||
|
|
122bd667dc | ||
|
|
21cd402204 | ||
|
|
0ae0439668 | ||
|
|
6dcc6313a6 | ||
|
|
78dbb59a00 | ||
|
|
7e40071571 | ||
|
|
90dc0e1702 | ||
|
|
2c18517121 | ||
|
|
d6c3588ed3 | ||
|
|
81dba3738e | ||
|
|
ad1cc6cff9 | ||
|
|
68d9d161f4 | ||
|
|
c66f99fcdc | ||
|
|
08b3f5f070 | ||
|
|
66d7d2549f | ||
|
|
d20392d413 | ||
|
|
58cc049a9f | ||
|
|
9b77ac128a | ||
|
|
e1738ea78e | ||
|
|
9bf13fc3d1 | ||
|
|
ab7e6f3f11 | ||
|
|
c5b1565337 | ||
|
|
d2e2d8438b | ||
|
|
23c3831ff9 | ||
|
|
296b008b9f | ||
|
|
31bf3874d6 | ||
|
|
e0c5ac1f02 | ||
|
|
e8f09d24c7 | ||
|
|
70f9fc8c7a | ||
|
|
f1c9812188 | ||
|
|
214242ff62 | ||
|
|
531ccca648 | ||
|
|
3d328b82ee | ||
|
|
d95b95038c | ||
|
|
ceb8c5d1e9 | ||
|
|
d1dd04e327 | ||
|
|
79cf550823 | ||
|
|
79905a1162 | ||
|
|
7d1357162e | ||
|
|
e4b5b92b82 | ||
|
|
ff97a97f08 | ||
|
|
f580f4484f | ||
|
|
7a5cf39d0d | ||
|
|
f81723ceac | ||
|
|
9ccc52cd04 | ||
|
|
5c7e960fa8 | ||
|
|
1a093ef482 | ||
|
|
472529af38 | ||
|
|
039def3b50 | ||
|
|
a78f8fa701 | ||
|
|
b3cc719add | ||
|
|
3fc8683585 | ||
|
|
78b90c3685 | ||
|
|
facafd8819 | ||
|
|
18edd79421 | ||
|
|
5d559141d5 | ||
|
|
f983962fc6 | ||
|
|
b997304bf6 | ||
|
|
9197dd14cc | ||
|
|
3c8d257b3e | ||
|
|
d32700c7b2 | ||
|
|
0de66386d4 | ||
|
|
9ae1161e85 | ||
|
|
03f7e4e577 | ||
|
|
f061d20c9d | ||
|
|
44d62b65d0 | ||
|
|
d53eb6fa11 | ||
|
|
23ec3c104a | ||
|
|
c200229f9e | ||
|
|
766ea96adf | ||
|
|
ffc67806ef | ||
|
|
32a1a3d1c0 | ||
|
|
1c0286e98a | ||
|
|
8f38afbf8e | ||
|
|
c3270af52b | ||
|
|
06eac9bbff | ||
|
|
dbcc34981a | ||
|
|
d4916a8be3 | ||
|
|
64d482ff48 | ||
|
|
25865f81ee | ||
|
|
545639ee44 | ||
|
|
23f37b05a3 | ||
|
|
1cff719015 | ||
|
|
548fa63e49 | ||
|
|
0476c8ebc6 | ||
|
|
1f7479466e | ||
|
|
d942a2ff56 | ||
|
|
90555c5cb2 | ||
|
|
b33c337baa | ||
|
|
77a92f326d | ||
|
|
5d731ca13f | ||
|
|
1c3c6b5382 | ||
|
|
76b0e578c5 | ||
|
|
090033ede5 | ||
|
|
9996d94b3c | ||
|
|
c8dd39fcbc | ||
|
|
539c5e44c5 | ||
|
|
4ee64681ad | ||
|
|
8e821d7aa8 | ||
|
|
3bb57504af | ||
|
|
4497bb0b81 | ||
|
|
0f12ead567 | ||
|
|
ab159f748b | ||
|
|
15b8665787 | ||
|
|
40ec8617ac | ||
|
|
ec9d13bce5 | ||
|
|
01e8ef7293 | ||
|
|
622d80c007 | ||
|
|
486cc9393c | ||
|
|
93324cc7b3 | ||
|
|
18109c63b0 | ||
|
|
b1fff4499f | ||
|
|
f0d6f173c9 | ||
|
|
ddebd30917 | ||
|
|
f50043f6cb | ||
|
|
a9f6cd41fd | ||
|
|
b75f81ec00 | ||
|
|
5055e00cf1 | ||
|
|
f371a1afd9 | ||
|
|
4950fe60bd | ||
|
|
9bb5a038e5 | ||
|
|
5381437664 | ||
|
|
4aa88bc2c0 | ||
|
|
dfcef3382e | ||
|
|
9e5954c598 | ||
|
|
8cfd775885 | ||
|
|
c13fab2a67 | ||
|
|
4001d0bf25 | ||
|
|
8d45d7e312 | ||
|
|
be5eadbecc | ||
|
|
95d43c54bf | ||
|
|
26f103473c | ||
|
|
adc5ffea99 | ||
|
|
5f6abcfa6f | ||
|
|
7c7e23d87a | ||
|
|
52d769d35c | ||
|
|
f04bc31820 | ||
|
|
9a2171e4ea | ||
|
|
8725b14056 | ||
|
|
eb32847d85 | ||
|
|
9dfcdbf478 | ||
|
|
e846481731 | ||
|
|
02a765743e | ||
|
|
e1309e1323 | ||
|
|
fb82299f5a | ||
|
|
116f55ff66 | ||
|
|
a029989aff | ||
|
|
57275a4912 | ||
|
|
a794963e2f |
17
.github/licenses.tmpl
vendored
Normal file
17
.github/licenses.tmpl
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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,5 +1,10 @@
|
||||
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,6 +20,10 @@ 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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: OpenBSD build cmd
|
||||
env:
|
||||
GOOS: openbsd
|
||||
|
||||
17
.github/workflows/cross-wasm.yml
vendored
17
.github/workflows/cross-wasm.yml
vendored
@@ -8,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,26 +19,27 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Wasm client build
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
run: go build ./cmd/tsconnect/wasm
|
||||
run: go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
|
||||
|
||||
- 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 build
|
||||
run: |
|
||||
./tool/go run ./cmd/tsconnect --fast-compression build
|
||||
./tool/go run ./cmd/tsconnect build-pkg
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
11
.github/workflows/cross-windows.yml
vendored
11
.github/workflows/cross-windows.yml
vendored
@@ -8,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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,21 +8,25 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: depaware tailscaled
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: depaware tailscale
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
- name: depaware
|
||||
run: go run github.com/tailscale/depaware --check
|
||||
tailscale.com/cmd/tailscaled
|
||||
tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/derper
|
||||
|
||||
64
.github/workflows/go-licenses.yml
vendored
Normal file
64
.github/workflows/go-licenses.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
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,21 +9,25 @@ 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.18
|
||||
|
||||
- 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
Normal file
35
.github/workflows/go_mod_tidy.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
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,18 +8,22 @@ 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: 1.18
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
go-version-file: go.mod
|
||||
|
||||
- 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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,16 +19,15 @@ 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: 1.18
|
||||
go-version-file: go.mod
|
||||
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
Normal file
112
.github/workflows/static-analysis.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
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'
|
||||
73
.github/workflows/staticcheck.yml
vendored
73
.github/workflows/staticcheck.yml
vendored
@@ -1,73 +0,0 @@
|
||||
name: staticcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- 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: 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:
|
||||
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'
|
||||
30
.github/workflows/tsconnect-pkg-publish.yml
vendored
Normal file
30
.github/workflows/tsconnect-pkg-publish.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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,6 +5,10 @@ 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 ]
|
||||
@@ -15,13 +19,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: 1.18
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
go-version-file: go.mod
|
||||
|
||||
- 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
77
.github/workflows/windows-race.yml
vendored
@@ -1,77 +0,0 @@
|
||||
name: Windows race
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.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,6 +8,10 @@ on:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
@@ -15,14 +19,13 @@ 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: 1.18.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
|
||||
19
Makefile
19
Makefile
@@ -9,15 +9,19 @@ vet:
|
||||
./tool/go vet ./...
|
||||
|
||||
tidy:
|
||||
./tool/go mod tidy -compat=1.17
|
||||
./tool/go mod tidy
|
||||
|
||||
updatedeps:
|
||||
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
|
||||
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
|
||||
./tool/go run github.com/tailscale/depaware --update \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper
|
||||
|
||||
depaware:
|
||||
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
./tool/go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
./tool/go run github.com/tailscale/depaware --check \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper
|
||||
|
||||
buildwindows:
|
||||
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
@@ -28,10 +32,13 @@ 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
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm
|
||||
|
||||
staticcheck:
|
||||
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -43,10 +43,7 @@ If your distro has conventions that preclude the use of
|
||||
`build_dist.sh`, please do the equivalent of what it does in your
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.18) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
We require the latest Go release, currently Go 1.19.
|
||||
|
||||
## Bugs
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.29.0
|
||||
1.30.0
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename.
|
||||
// into filename. The perm argument is ignored on Windows.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
if err != nil {
|
||||
|
||||
@@ -11,15 +11,31 @@ 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)}
|
||||
b := &BIRDClient{
|
||||
socket: socket,
|
||||
conn: conn,
|
||||
scanner: bufio.NewScanner(conn),
|
||||
timeNow: time.Now,
|
||||
timeout: timeout,
|
||||
}
|
||||
// Read and discard the first line as that is the welcome message.
|
||||
if _, err := b.readResponse(); err != nil {
|
||||
return nil, err
|
||||
@@ -32,6 +48,8 @@ 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.
|
||||
@@ -81,10 +99,15 @@ 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
|
||||
}
|
||||
fmt.Fprintln(b.conn)
|
||||
if _, err := fmt.Fprintln(b.conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.readResponse()
|
||||
}
|
||||
|
||||
@@ -105,14 +128,20 @@ 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() {
|
||||
return "", fmt.Errorf("reading response from bird failed: %q", resp.String())
|
||||
}
|
||||
if err := b.scanner.Err(); err != nil {
|
||||
return "", err
|
||||
if err := b.scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("reading response from bird failed (EOF): %q", resp.String())
|
||||
}
|
||||
out := b.scanner.Bytes()
|
||||
if _, err := resp.Write(out); err != nil {
|
||||
|
||||
@@ -8,9 +8,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeBIRD struct {
|
||||
@@ -109,3 +112,82 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -680,6 +681,42 @@ 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.
|
||||
//
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
//go:build !go1.19
|
||||
// +build !go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
func init() {
|
||||
you_need_Go_1_18_to_compile_Tailscale()
|
||||
you_need_Go_1_19_to_compile_Tailscale()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package tailscale
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
// Package tailscale contains Go clients for the Tailscale Local API and
|
||||
// Tailscale control plane API.
|
||||
|
||||
@@ -153,18 +153,25 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
}
|
||||
writef("}")
|
||||
case *types.Map:
|
||||
elem := ft.Elem()
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(ft.Elem()))
|
||||
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(elem))
|
||||
if sliceType, isSlice := elem.(*types.Slice); isSlice {
|
||||
n := it.QualifiedName(sliceType.Elem())
|
||||
writef("\tfor k := range src.%s {", fname)
|
||||
// use zero-length slice instead of nil to ensure
|
||||
// the key is always copied.
|
||||
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
|
||||
writef("\t}")
|
||||
} else if codegen.ContainsPointers(ft.Elem()) {
|
||||
} else if codegen.ContainsPointers(elem) {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
switch elem.(type) {
|
||||
case *types.Pointer:
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
default:
|
||||
writef("\t\tv2 := v.Clone()")
|
||||
writef("\t\tdst.%s[k] = *v2", fname)
|
||||
}
|
||||
writef("\t}")
|
||||
} else {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
var dnsCache atomic.Value // of []byte
|
||||
var dnsCache syncs.AtomicValue[[]byte]
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
@@ -58,7 +59,7 @@ func refreshBootstrapDNS() {
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
j, _ := dnsCache.Load().([]byte)
|
||||
j := dnsCache.Load()
|
||||
// Bootstrap DNS requests occur cross-regions,
|
||||
// and are randomized per request,
|
||||
// so keeping a connection open is pointlessly expensive.
|
||||
|
||||
@@ -20,6 +20,11 @@ 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
|
||||
@@ -87,7 +92,13 @@ 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 m.cert, nil
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
|
||||
197
cmd/derper/depaware.txt
Normal file
197
cmd/derper/depaware.txt
Normal file
@@ -0,0 +1,197 @@
|
||||
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+
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -356,7 +357,8 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
|
||||
} else {
|
||||
stunIPv6.Add(1)
|
||||
}
|
||||
res := stun.Response(txid, ua.IP, uint16(ua.Port))
|
||||
addr, _ := netip.AddrFromSlice(ua.IP)
|
||||
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port)))
|
||||
_, err = pc.WriteTo(res, ua)
|
||||
if err != nil {
|
||||
stunWriteError.Add(1)
|
||||
|
||||
@@ -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,6 +8,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
@@ -30,17 +31,14 @@ 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("::error 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("::warning 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 {
|
||||
@@ -207,10 +205,6 @@ func main() {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(modifiedExternallyFailure) != 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func sumFile(fname string) (string, error) {
|
||||
@@ -271,13 +265,16 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
data, err := os.ReadFile(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err = hujson.Standardize(data)
|
||||
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), fin)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -63,17 +63,24 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
_, tailnet, ok := strings.Cut(info.Node.Name, info.Node.ComputedName+".")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
}
|
||||
tailnet, _, ok = strings.Cut(tailnet, ".beta.tailscale.net")
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
|
||||
return
|
||||
// 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, _, 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 {
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
//
|
||||
// Use this Grafana configuration to enable the auth proxy:
|
||||
//
|
||||
// [auth.proxy]
|
||||
// enabled = true
|
||||
// header_name = X-WEBAUTH-USER
|
||||
// header_property = username
|
||||
// auto_sign_up = true
|
||||
// whitelist = 127.0.0.1
|
||||
// headers = Name:X-WEBAUTH-NAME
|
||||
// enable_login_token = true
|
||||
// [auth.proxy]
|
||||
// enabled = true
|
||||
// header_name = X-WEBAUTH-USER
|
||||
// header_property = username
|
||||
// auto_sign_up = true
|
||||
// whitelist = 127.0.0.1
|
||||
// headers = Name:X-WEBAUTH-NAME
|
||||
// enable_login_token = true
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -170,6 +169,8 @@ change in the future.
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
netlockCmd,
|
||||
licensesCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
@@ -230,8 +231,6 @@ var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
@@ -257,7 +256,6 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
||||
return
|
||||
}
|
||||
gotSignal.Set(true)
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
@@ -62,11 +62,14 @@ 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
|
||||
}
|
||||
if isDSM6 {
|
||||
fmt.Printf("/dev/net/tun exists and has permissions 0666. Skipping setcap on DSM6.\n")
|
||||
printf("/dev/net/tun exists and has permissions 0666. Skipping setcap on DSM6.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,6 +83,6 @@ func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if out, err := exec.Command("/bin/setcap", "cap_net_admin,cap_net_raw+eip", daemonBin).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("setcap: %v, %s", err, out)
|
||||
}
|
||||
fmt.Printf("Done. To restart Tailscale to use the new permissions, run:\n\n sudo synosystemctl restart pkgctl-Tailscale.service\n\n")
|
||||
printf("Done. To restart Tailscale to use the new permissions, run:\n\n sudo synosystemctl restart pkgctl-Tailscale.service\n\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -308,18 +308,18 @@ func runStat(ctx context.Context, args []string) error {
|
||||
for _, a := range args {
|
||||
fi, err := os.Lstat(a)
|
||||
if err != nil {
|
||||
fmt.Printf("%s: %v\n", a, err)
|
||||
printf("%s: %v\n", a, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s: %v, %v\n", a, fi.Mode(), fi.Size())
|
||||
printf("%s: %v, %v\n", a, fi.Mode(), fi.Size())
|
||||
if fi.IsDir() {
|
||||
ents, _ := os.ReadDir(a)
|
||||
for i, ent := range ents {
|
||||
if i == 25 {
|
||||
fmt.Printf(" ...\n")
|
||||
printf(" ...\n")
|
||||
break
|
||||
}
|
||||
fmt.Printf(" - %s\n", ent.Name())
|
||||
printf(" - %s\n", ent.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ func runVia(ctx context.Context, args []string) error {
|
||||
v4 := tsaddr.UnmapVia(ipp.Addr())
|
||||
a := ipp.Addr().As16()
|
||||
siteID := binary.BigEndian.Uint32(a[8:12])
|
||||
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netip.PrefixFrom(v4, ipp.Bits()-96))
|
||||
printf("site %v (0x%x), %v\n", siteID, siteID, netip.PrefixFrom(v4, ipp.Bits()-96))
|
||||
case 2:
|
||||
siteID, err := strconv.ParseUint(args[0], 0, 32)
|
||||
if err != nil {
|
||||
@@ -437,7 +437,7 @@ func runVia(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(via)
|
||||
outln(via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -489,7 +489,7 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,7 +7,6 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
@@ -29,6 +28,6 @@ func runIDToken(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(tr.IDToken)
|
||||
outln(tr.IDToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
42
cmd/tailscale/cli/licenses.go
Normal file
42
cmd/tailscale/cli/licenses.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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,
|
||||
}
|
||||
|
||||
func runLicenses(ctx context.Context, args []string) error {
|
||||
var licenseURL string
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
licenseURL = "https://tailscale.com/licenses/android"
|
||||
case "darwin", "ios":
|
||||
licenseURL = "https://tailscale.com/licenses/apple"
|
||||
case "windows":
|
||||
licenseURL = "https://tailscale.com/licenses/windows"
|
||||
default:
|
||||
licenseURL = "https://tailscale.com/licenses/tailscale"
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
` + licenseURL)
|
||||
return nil
|
||||
}
|
||||
101
cmd/tailscale/cli/network-lock.go
Normal file
101
cmd/tailscale/cli/network-lock.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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
|
||||
}
|
||||
@@ -55,8 +55,8 @@ func presentRiskToUser(riskType, riskMessage string) error {
|
||||
if riskAccepted(riskType) {
|
||||
return nil
|
||||
}
|
||||
fmt.Println(riskMessage)
|
||||
fmt.Printf("To skip this warning, use --accept-risk=%s\n", riskType)
|
||||
outln(riskMessage)
|
||||
printf("To skip this warning, use --accept-risk=%s\n", riskType)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT)
|
||||
@@ -64,15 +64,15 @@ func presentRiskToUser(riskType, riskMessage string) error {
|
||||
for left := riskAbortTimeSeconds; left > 0; left-- {
|
||||
msg := fmt.Sprintf("\rContinuing in %d seconds...", left)
|
||||
msgLen = len(msg)
|
||||
fmt.Print(msg)
|
||||
printf(msg)
|
||||
select {
|
||||
case <-interrupt:
|
||||
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen+1))
|
||||
printf("\r%s\r", strings.Repeat("x", msgLen+1))
|
||||
return errAborted
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen))
|
||||
printf("\r%s\r", strings.Repeat(" ", msgLen))
|
||||
return errAborted
|
||||
}
|
||||
|
||||
@@ -178,15 +178,16 @@ var upArgs upArgsT
|
||||
// JSON block will be output. The AuthURL and QR fields will not be present, the
|
||||
// BackendState and Error fields will give the result of the authentication.
|
||||
// Ex:
|
||||
// {
|
||||
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
|
||||
// "QR": "data:image/png;base64,0123...cdef"
|
||||
// "BackendState": "NeedsLogin"
|
||||
// }
|
||||
// {
|
||||
// "BackendState": "Running"
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
|
||||
// "QR": "data:image/png;base64,0123...cdef"
|
||||
// "BackendState": "NeedsLogin"
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "BackendState": "Running"
|
||||
// }
|
||||
type upOutputJSON struct {
|
||||
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
|
||||
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
|
||||
@@ -251,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 IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
return nil, fmt.Errorf("%s advertised without its IPv4 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if advertiseDefaultRoute {
|
||||
@@ -570,9 +571,9 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
log.Printf("upOutputJSON marshalling error: %v", err)
|
||||
printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
outln(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
@@ -710,7 +711,7 @@ func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
if err != nil {
|
||||
log.Printf("printUpDoneJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
outln(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netaddr from tailscale.com/disco+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/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+
|
||||
@@ -71,9 +72,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/interfaces+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/types/key
|
||||
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
|
||||
@@ -89,6 +90,7 @@ 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+
|
||||
@@ -97,7 +99,9 @@ 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+
|
||||
@@ -114,12 +118,15 @@ 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+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
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+
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/netaddr from tailscale.com/disco+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
@@ -227,6 +227,7 @@ 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
|
||||
@@ -241,10 +242,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/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/types/key
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
|
||||
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+
|
||||
@@ -263,6 +264,7 @@ 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+
|
||||
@@ -271,6 +273,7 @@ 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+
|
||||
@@ -278,6 +281,7 @@ 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+
|
||||
L 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+
|
||||
@@ -292,6 +296,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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
|
||||
@@ -311,6 +316,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device+
|
||||
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/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+
|
||||
@@ -318,8 +325,9 @@ 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+
|
||||
@@ -397,6 +405,7 @@ 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+
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
// HTTP proxy code
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
//go:build !go1.19
|
||||
// +build !go1.19
|
||||
|
||||
package main
|
||||
|
||||
func init() {
|
||||
you_need_Go_1_18_to_compile_Tailscale()
|
||||
you_need_Go_1_19_to_compile_Tailscale()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
// The tailscaled program is the Tailscale client daemon. It's configured
|
||||
// and controlled via the tailscale CLI program.
|
||||
@@ -481,7 +481,7 @@ func shouldWrapNetstack() bool {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin", "freebsd":
|
||||
case "windows", "darwin", "freebsd", "openbsd":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
return true
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18 && (linux || darwin || freebsd || openbsd)
|
||||
// +build go1.18
|
||||
//go:build go1.19 && (linux || darwin || freebsd || openbsd)
|
||||
// +build go1.19
|
||||
// +build linux darwin freebsd openbsd
|
||||
|
||||
package main
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && go1.18
|
||||
// +build !windows,go1.18
|
||||
//go:build !windows && go1.19
|
||||
// +build !windows,go1.19
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
|
||||
5
cmd/tsconnect/.gitignore
vendored
5
cmd/tsconnect/.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
src/wasm_exec.js
|
||||
src/main.wasm
|
||||
node_modules/
|
||||
dist/
|
||||
/dist
|
||||
/pkg
|
||||
|
||||
40
cmd/tsconnect/README.md
Normal file
40
cmd/tsconnect/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# tsconnect
|
||||
|
||||
The tsconnect command builds and serves the static site that is generated for
|
||||
the Tailscale Connect JS/WASM client.
|
||||
|
||||
## Development
|
||||
|
||||
To start the development server:
|
||||
|
||||
```
|
||||
./tool/go run ./cmd/tsconnect dev
|
||||
```
|
||||
|
||||
The site is served at http://localhost:9090/. JavaScript and CSS changes can be picked up with a browser reload. Go changes (including to the `wasm` package) require the server to be stopped and restarted. In development mode the state the Tailscale client is stored in `sessionStorage` and will thus survive page reloads (but not the tab being closed).
|
||||
|
||||
## Deployment
|
||||
|
||||
To build the static assets necessary for serving, run:
|
||||
|
||||
```
|
||||
./tool/go run ./cmd/tsconnect build
|
||||
```
|
||||
|
||||
To serve them, run:
|
||||
|
||||
```
|
||||
./tool/go run ./cmd/tsconnect serve
|
||||
```
|
||||
|
||||
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).
|
||||
79
cmd/tsconnect/build-pkg.go
Normal file
79
cmd/tsconnect/build-pkg.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"github.com/tailscale/hujson"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func runBuildPkg() {
|
||||
buildOptions, err := commonSetup(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.EntryPoints = []string{"src/pkg/pkg.ts", "src/pkg/pkg.css"}
|
||||
buildOptions.Outdir = *pkgDir
|
||||
buildOptions.Format = esbuild.FormatESModule
|
||||
buildOptions.AssetNames = "[name]"
|
||||
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)
|
||||
}
|
||||
@@ -5,21 +5,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/util/precompress"
|
||||
)
|
||||
|
||||
func runBuild() {
|
||||
@@ -33,7 +27,7 @@ func runBuild() {
|
||||
log.Fatalf("Linting failed: %v", err)
|
||||
}
|
||||
|
||||
if err := cleanDist(); err != nil {
|
||||
if err := cleanDir(*distDir, "placeholder"); err != nil {
|
||||
log.Fatalf("Cannot clean %s: %v", *distDir, err)
|
||||
}
|
||||
|
||||
@@ -46,21 +40,7 @@ func runBuild() {
|
||||
buildOptions.AssetNames = "[name]-[hash]"
|
||||
buildOptions.Metafile = true
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
result := runEsbuild(*buildOptions)
|
||||
|
||||
// Preserve build metadata so we can extract hashed file names for serving.
|
||||
metadataBytes, err := fixEsbuildMetadataPaths(result.Metafile)
|
||||
@@ -71,7 +51,7 @@ func runBuild() {
|
||||
log.Fatalf("Cannot write metadata: %v", err)
|
||||
}
|
||||
|
||||
if er := precompressDist(); err != nil {
|
||||
if er := precompressDist(*fastCompression); err != nil {
|
||||
log.Fatalf("Cannot precompress resources: %v", er)
|
||||
}
|
||||
}
|
||||
@@ -103,8 +83,6 @@ 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)
|
||||
@@ -125,71 +103,12 @@ func cleanDist() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func precompressDist() error {
|
||||
func precompressDist(fastCompression bool) error {
|
||||
log.Printf("Pre-compressing files in %s/...\n", *distDir)
|
||||
var eg errgroup.Group
|
||||
err := fs.WalkDir(os.DirFS(*distDir), ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !compressibleExtensions[filepath.Ext(p)] {
|
||||
return nil
|
||||
}
|
||||
p = path.Join(*distDir, p)
|
||||
log.Printf("Pre-compressing %v\n", p)
|
||||
|
||||
eg.Go(func() error {
|
||||
return precompress(p)
|
||||
})
|
||||
return nil
|
||||
return precompress.PrecompressDir(*distDir, precompress.Options{
|
||||
FastCompression: fastCompression,
|
||||
ProgressFn: func(path string) {
|
||||
log.Printf("Pre-compressing %v\n", path)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
var compressibleExtensions = map[string]bool{
|
||||
".js": true,
|
||||
".css": true,
|
||||
".wasm": true,
|
||||
}
|
||||
|
||||
func precompress(path string) error {
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
|
||||
return gzip.NewWriterLevel(w, gzip.BestCompression)
|
||||
}, path+".gz", fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
|
||||
return brotli.NewWriterLevel(w, brotli.BestCompression), nil
|
||||
}, path+".br", fi.Mode())
|
||||
}
|
||||
|
||||
func writeCompressed(contents []byte, compressedWriterCreator func(io.Writer) (io.WriteCloser, error), outputPath string, outputMode fs.FileMode) error {
|
||||
var buf bytes.Buffer
|
||||
compressedWriter, err := compressedWriterCreator(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := compressedWriter.Write(contents); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compressedWriter.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(outputPath, buf.Bytes(), outputMode)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,73 +34,152 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
|
||||
return nil, fmt.Errorf("Cannot change cwd: %w", err)
|
||||
}
|
||||
}
|
||||
if err := buildDeps(dev); err != nil {
|
||||
return nil, fmt.Errorf("Cannot build deps: %w", err)
|
||||
if err := installJSDeps(); err != nil {
|
||||
return nil, fmt.Errorf("Cannot install JS deps: %w", err)
|
||||
}
|
||||
|
||||
return &esbuild.BuildOptions{
|
||||
EntryPoints: []string{"src/index.ts", "src/index.css"},
|
||||
Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
|
||||
EntryPoints: []string{"src/app/index.ts", "src/app/index.css"},
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
if err := installJSDeps(); err != nil {
|
||||
return fmt.Errorf("Cannot install JS deps: %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
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
wasmExecDstPath := filepath.Join("src", "wasm_exec.js")
|
||||
contents, err := os.ReadFile(wasmExecSrcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
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")
|
||||
}
|
||||
return os.WriteFile(wasmExecDstPath, contents, 0600)
|
||||
if len(result.Warnings) > 0 {
|
||||
log.Printf("ESBuild Warnings:\n")
|
||||
for _, w := range result.Warnings {
|
||||
log.Printf("%v", w)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 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")
|
||||
// setupEsbuildWasmExecJS generates an esbuild plugin that serves the current
|
||||
// wasm_exec.js runtime helper library from the Go toolchain.
|
||||
func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
|
||||
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 := ioutil.TempFile("", "main.*.wasm")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
|
||||
}
|
||||
outputPath := outputFile.Name()
|
||||
defer os.Remove(outputPath)
|
||||
|
||||
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", "src/main.wasm", "./wasm")
|
||||
|
||||
args = append(args, "-o", outputPath, "./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
|
||||
return cmd.Run()
|
||||
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)
|
||||
}
|
||||
|
||||
// installJSDeps installs the JavaScript dependencies specified by package.json
|
||||
@@ -119,13 +200,16 @@ func runYarn(args ...string) error {
|
||||
// from entry points to hashed file names.
|
||||
type EsbuildMetadata struct {
|
||||
Outputs map[string]struct {
|
||||
Inputs map[string]struct {
|
||||
BytesInOutput int64 `json:"bytesInOutput"`
|
||||
} `json:"inputs,omitempty"`
|
||||
EntryPoint string `json:"entryPoint,omitempty"`
|
||||
} `json:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
func setupEsbuildTailwind(build esbuild.PluginBuild, dev bool) {
|
||||
build.OnLoad(esbuild.OnLoadOptions{
|
||||
Filter: "./src/index.css$",
|
||||
Filter: "./src/.*\\.css$",
|
||||
}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
|
||||
start := time.Now()
|
||||
yarnArgs := []string{"--silent", "tailwind", "-i", args.Path}
|
||||
|
||||
@@ -3,36 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tailscale Connect</title>
|
||||
<link rel="stylesheet" type="text/css" href="dist/index.css" />
|
||||
<script src="dist/index.js" defer></script>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2 mb-6">
|
||||
<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" id="state">Loading…</div>
|
||||
<div class="text-gray-600">Loading…</div>
|
||||
</header>
|
||||
</div>
|
||||
<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>
|
||||
<script src="dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
"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": "^4.18.0"
|
||||
"xterm": "^4.18.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit"
|
||||
"lint": "tsc --noEmit",
|
||||
"pkg-types": "dts-bundle-generator --inline-declare-global=true --no-banner -o pkg/pkg.d.ts src/pkg/pkg.ts"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
|
||||
17
cmd/tsconnect/package.json.tmpl
Normal file
17
cmd/tsconnect/package.json.tmpl
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/util/precompress"
|
||||
)
|
||||
|
||||
//go:embed index.html
|
||||
@@ -83,10 +84,19 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
return nil, fmt.Errorf("Could not parse esbuild-metadata.json: %w", err)
|
||||
}
|
||||
entryPointsToHashedDistPaths := make(map[string]string)
|
||||
mainWasmPath := ""
|
||||
for outputPath, output := range esbuildMetadata.Outputs {
|
||||
if output.EntryPoint != "" {
|
||||
entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
|
||||
}
|
||||
if path.Ext(outputPath) == ".wasm" {
|
||||
for input := range output.Inputs {
|
||||
if input == "src/main.wasm" {
|
||||
mainWasmPath = path.Join("dist", outputPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indexBytes := rawIndexBytes
|
||||
@@ -96,39 +106,25 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
|
||||
}
|
||||
}
|
||||
if mainWasmPath != "" {
|
||||
mainWasmPrefetch := fmt.Sprintf("</title>\n<link rel='preload' as='fetch' crossorigin='anonymous' href='%s'>", mainWasmPath)
|
||||
indexBytes = bytes.ReplaceAll(indexBytes, []byte("</title>"), []byte(mainWasmPrefetch))
|
||||
}
|
||||
|
||||
return indexBytes, nil
|
||||
}
|
||||
|
||||
var entryPointsToDefaultDistPaths = map[string]string{
|
||||
"src/index.css": "dist/index.css",
|
||||
"src/index.ts": "dist/index.js",
|
||||
"src/app/index.css": "dist/index.css",
|
||||
"src/app/index.ts": "dist/index.js",
|
||||
}
|
||||
|
||||
func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
|
||||
path := r.URL.Path
|
||||
var f fs.File
|
||||
// Prefer pre-compressed versions generated during the build step.
|
||||
if tsweb.AcceptsEncoding(r, "br") {
|
||||
if brotliFile, err := distFS.Open(path + ".br"); err == nil {
|
||||
f = brotliFile
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
}
|
||||
}
|
||||
if f == nil && tsweb.AcceptsEncoding(r, "gzip") {
|
||||
if gzipFile, err := distFS.Open(path + ".gz"); err == nil {
|
||||
f = gzipFile
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
if rawFile, err := distFS.Open(path); err == nil {
|
||||
f = rawFile
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
f, err := precompress.OpenPrecompressedFile(w, r, path, distFS)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
||||
123
cmd/tsconnect/src/app/app.tsx
Normal file
123
cmd/tsconnect/src/app/app.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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) => {
|
||||
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
|
||||
)
|
||||
})
|
||||
}
|
||||
21
cmd/tsconnect/src/app/go-panic-display.tsx
Normal file
21
cmd/tsconnect/src/app/go-panic-display.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
38
cmd/tsconnect/src/app/header.tsx
Normal file
38
cmd/tsconnect/src/app/header.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
37
cmd/tsconnect/src/app/index.ts
Normal file
37
cmd/tsconnect/src/app/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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()
|
||||
105
cmd/tsconnect/src/app/ssh.tsx
Normal file
105
cmd/tsconnect/src/app/ssh.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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 } from "preact/hooks"
|
||||
import { runSSHSession, SSHSessionDef } from "../lib/ssh"
|
||||
|
||||
export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
|
||||
const [sshSessionDef, setSSHSessionDef] = useState<SSHSessionDef | null>(null)
|
||||
const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
|
||||
if (sshSessionDef) {
|
||||
return (
|
||||
<SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
|
||||
)
|
||||
}
|
||||
const sshPeers = netMap.peers.filter(
|
||||
(p) => p.tailscaleSSHEnabled && p.online !== false
|
||||
)
|
||||
|
||||
if (sshPeers.length == 0) {
|
||||
return <NoSSHPeers />
|
||||
}
|
||||
|
||||
return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} />
|
||||
}
|
||||
|
||||
function SSHSession({
|
||||
def,
|
||||
ipn,
|
||||
onDone,
|
||||
}: {
|
||||
def: SSHSessionDef
|
||||
ipn: IPN
|
||||
onDone: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="flex-grow bg-black p-2 overflow-hidden"
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
// Run the SSH session aysnchronously, so that the React render
|
||||
// loop is complete (otherwise the SSH form may still be visible,
|
||||
// which affects the size of the terminal, leading to a spurious
|
||||
// initial resize).
|
||||
setTimeout(() => runSSHSession(node, def, ipn, onDone), 0)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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: SSHSessionDef) => 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"
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
32
cmd/tsconnect/src/app/url-display.tsx
Normal file
32
cmd/tsconnect/src/app/url-display.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +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 { 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
|
||||
65
cmd/tsconnect/src/lib/ssh.ts
Normal file
65
cmd/tsconnect/src/lib/ssh.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Terminal } from "xterm"
|
||||
import { FitAddon } from "xterm-addon-fit"
|
||||
|
||||
export type SSHSessionDef = {
|
||||
username: string
|
||||
hostname: string
|
||||
}
|
||||
|
||||
export function runSSHSession(
|
||||
termContainerNode: HTMLDivElement,
|
||||
def: SSHSessionDef,
|
||||
ipn: IPN,
|
||||
onDone: () => void
|
||||
) {
|
||||
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()
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let handleBeforeUnload: ((e: BeforeUnloadEvent) => void) | undefined
|
||||
|
||||
const sshSession = ipn.ssh(def.hostname + "2", 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) {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
}
|
||||
onDone()
|
||||
},
|
||||
})
|
||||
|
||||
// Make terminal and SSH session track the size of the containing DOM node.
|
||||
resizeObserver = new 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()
|
||||
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||
}
|
||||
@@ -1,73 +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 * as qrcode from "qrcode"
|
||||
|
||||
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))
|
||||
|
||||
document.body.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
|
||||
@@ -1,65 +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 {
|
||||
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.getElementById("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)
|
||||
}
|
||||
9
cmd/tsconnect/src/pkg/pkg.css
Normal file
9
cmd/tsconnect/src/pkg/pkg.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/* 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;
|
||||
41
cmd/tsconnect/src/pkg/pkg.ts
Normal file
41
cmd/tsconnect/src/pkg/pkg.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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"
|
||||
@@ -1,80 +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 { Terminal } from "xterm"
|
||||
|
||||
export function showSSHForm(peers: IPNNetMapPeerNode[], ipn: IPN) {
|
||||
const formNode = document.getElementById("ssh-form") as HTMLDivElement
|
||||
const noSSHNode = document.getElementById("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.getElementById("ssh-form") as HTMLDivElement
|
||||
formNode.classList.add("hidden")
|
||||
}
|
||||
|
||||
function ssh(hostname: string, username: string, ipn: IPN) {
|
||||
const termContainerNode = document.createElement("div")
|
||||
termContainerNode.className = "p-3"
|
||||
document.body.appendChild(termContainerNode)
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
})
|
||||
term.open(termContainerNode)
|
||||
|
||||
// Cancel wheel events from scrolling the page if the terminal has scrollback
|
||||
termContainerNode.addEventListener("wheel", (e) => {
|
||||
if (term.buffer.active.baseY > 0) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
let onDataHook: ((data: string) => void) | undefined
|
||||
term.onData((e) => {
|
||||
onDataHook?.(e)
|
||||
})
|
||||
|
||||
term.focus()
|
||||
|
||||
ipn.ssh(hostname, username, {
|
||||
writeFn: (input) => term.write(input),
|
||||
setReadFn: (hook) => (onDataHook = hook),
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
onDone: () => {
|
||||
term.dispose()
|
||||
termContainerNode.remove()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
/**
|
||||
* @fileoverview Type definitions for types exported by the wasm_js.go Go
|
||||
* 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)
|
||||
* module.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
@@ -20,12 +19,23 @@ 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
|
||||
}
|
||||
): void
|
||||
): IPNSSHSession
|
||||
fetch(url: string): Promise<{
|
||||
status: number
|
||||
statusText: string
|
||||
text: () => Promise<string>
|
||||
}>
|
||||
}
|
||||
|
||||
interface IPNSSHSession {
|
||||
resize(rows: number, cols: number): boolean
|
||||
close(): boolean
|
||||
}
|
||||
|
||||
interface IPNStateStorage {
|
||||
@@ -35,6 +45,8 @@ declare global {
|
||||
|
||||
type IPNConfig = {
|
||||
stateStorage?: IPNStateStorage
|
||||
authKey?: string
|
||||
controlURL?: string
|
||||
}
|
||||
|
||||
type IPNCallbacks = {
|
||||
@@ -64,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"
|
||||
}
|
||||
|
||||
/** 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,
|
||||
}
|
||||
export {}
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.ts"],
|
||||
content: ["./index.html", "./src/**/*.ts", "./src/**/*.tsx"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
// The tsconnect command builds and serves the static site that is generated for
|
||||
// the Tailscale Connect JS/WASM client. Can be run in 3 modes:
|
||||
// - dev: builds the site and serves it. JS and CSS changes can be picked up
|
||||
// with a reload.
|
||||
// - build: builds the site and writes it to dist/
|
||||
// - serve: serves the site from dist/ (embedded in the binary)
|
||||
// - dev: builds the site and serves it. JS and CSS changes can be picked up
|
||||
// with a reload.
|
||||
// - build: builds the site and writes it to dist/
|
||||
// - serve: serves the site from dist/ (embedded in the binary)
|
||||
package main // import "tailscale.com/cmd/tsconnect"
|
||||
|
||||
import (
|
||||
@@ -18,9 +18,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":9090", "address to listen on")
|
||||
distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
|
||||
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
|
||||
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() {
|
||||
@@ -35,6 +38,8 @@ func main() {
|
||||
runDev()
|
||||
case "build":
|
||||
runBuild()
|
||||
case "build-pkg":
|
||||
runBuildPkg()
|
||||
case "serve":
|
||||
runServe()
|
||||
default:
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
@@ -30,16 +31,20 @@ 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 {
|
||||
@@ -55,7 +60,37 @@ func main() {
|
||||
|
||||
func newIPN(jsConfig js.Value) map[string]any {
|
||||
netns.SetEnabled(false)
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
jsStateStorage := jsConfig.Get("stateStorage")
|
||||
var store ipn.StateStore
|
||||
if jsStateStorage.IsUndefined() {
|
||||
store = new(mem.Store)
|
||||
} else {
|
||||
store = &jsStateStore{jsStateStorage}
|
||||
}
|
||||
|
||||
jsControlURL := jsConfig.Get("controlURL")
|
||||
controlURL := ControlURL
|
||||
if jsControlURL.Type() == js.TypeString {
|
||||
controlURL = jsControlURL.String()
|
||||
}
|
||||
|
||||
jsAuthKey := jsConfig.Get("authKey")
|
||||
var authKey string
|
||||
if jsAuthKey.Type() == js.TypeString {
|
||||
authKey = jsAuthKey.String()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
dialer := new(tsdial.Dialer)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
@@ -85,14 +120,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
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{
|
||||
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, nil, ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
LoginFlags: controlclient.LoginEphemeral,
|
||||
})
|
||||
@@ -100,11 +128,14 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
log.Fatalf("ipnserver.New: %v", err)
|
||||
}
|
||||
lb := srv.LocalBackend()
|
||||
ns.SetLocalBackend(lb)
|
||||
|
||||
jsIPN := &jsIPN{
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
controlURL: controlURL,
|
||||
authKey: authKey,
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
@@ -142,24 +173,51 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
log.Printf("Usage: ssh(hostname, userName, termConfig)")
|
||||
return nil
|
||||
}
|
||||
go jsIPN.ssh(
|
||||
return jsIPN.ssh(
|
||||
args[0].String(),
|
||||
args[1].String(),
|
||||
args[2])
|
||||
return nil
|
||||
}),
|
||||
"fetch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 1 {
|
||||
log.Printf("Usage: fetch(url)")
|
||||
return nil
|
||||
}
|
||||
|
||||
url := args[0].String()
|
||||
return jsIPN.fetch(url)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type jsIPN struct {
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
controlURL string
|
||||
authKey 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",
|
||||
}
|
||||
|
||||
func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
notifyState := func(state ipn.State) {
|
||||
jsCallbacks.Call("notifyState", int(state))
|
||||
jsCallbacks.Call("notifyState", jsIPNState[state])
|
||||
}
|
||||
notifyState(ipn.NoState)
|
||||
|
||||
@@ -178,7 +236,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
if n.State != nil {
|
||||
notifyState(*n.State)
|
||||
}
|
||||
if nm := n.NetMap; nm != nil && i.lb.State() == ipn.Running {
|
||||
if nm := n.NetMap; nm != nil {
|
||||
jsNetMap := jsNetMap{
|
||||
Self: jsNetMapSelfNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
@@ -187,7 +245,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
NodeKey: nm.NodeKey.String(),
|
||||
MachineKey: nm.MachineKey.String(),
|
||||
},
|
||||
MachineStatus: int(nm.MachineStatus),
|
||||
MachineStatus: jsMachineStatus[nm.MachineStatus],
|
||||
},
|
||||
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
|
||||
name := p.Name
|
||||
@@ -222,12 +280,13 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
err := i.lb.Start(ipn.Options{
|
||||
StateKey: "wasm",
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
ControlURL: i.controlURL,
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: generateHostname(),
|
||||
},
|
||||
AuthKey: i.authKey,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Start error: %v", err)
|
||||
@@ -256,25 +315,55 @@ func (i *jsIPN) logout() {
|
||||
go i.lb.Logout()
|
||||
}
|
||||
|
||||
func (i *jsIPN) ssh(host, username string, termConfig js.Value) {
|
||||
writeFn := termConfig.Get("writeFn")
|
||||
setReadFn := termConfig.Get("setReadFn")
|
||||
rows := termConfig.Get("rows").Int()
|
||||
cols := termConfig.Get("cols").Int()
|
||||
onDone := termConfig.Get("onDone")
|
||||
func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
||||
jsSSHSession := &jsSSHSession{
|
||||
jsIPN: i,
|
||||
host: host,
|
||||
username: username,
|
||||
termConfig: termConfig,
|
||||
}
|
||||
|
||||
go jsSSHSession.Run()
|
||||
|
||||
return map[string]any{
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
return jsSSHSession.Close() != nil
|
||||
}),
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
rows := args[0].Int()
|
||||
cols := args[1].Int()
|
||||
return jsSSHSession.Resize(rows, cols) != nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type jsSSHSession struct {
|
||||
jsIPN *jsIPN
|
||||
host string
|
||||
username string
|
||||
termConfig js.Value
|
||||
session *ssh.Session
|
||||
}
|
||||
|
||||
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) {
|
||||
write(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c, err := i.dialer.UserDial(ctx, "tcp", net.JoinHostPort(host, "22"))
|
||||
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
|
||||
if err != nil {
|
||||
writeError("Dial", err)
|
||||
return
|
||||
@@ -283,10 +372,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) {
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
User: username,
|
||||
User: s.username,
|
||||
}
|
||||
|
||||
sshConn, _, _, err := ssh.NewClientConn(c, host, config)
|
||||
sshConn, _, _, err := ssh.NewClientConn(c, s.host, config)
|
||||
if err != nil {
|
||||
writeError("SSH Connection", err)
|
||||
return
|
||||
@@ -302,6 +391,7 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) {
|
||||
writeError("SSH Session", err)
|
||||
return
|
||||
}
|
||||
s.session = session
|
||||
write("Session Established\r\n")
|
||||
defer session.Close()
|
||||
|
||||
@@ -338,11 +428,49 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) {
|
||||
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
writeError("Exit", err)
|
||||
writeError("Wait", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) Close() error {
|
||||
return s.session.Close()
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) Resize(rows, cols int) error {
|
||||
return s.session.WindowChange(rows, cols)
|
||||
}
|
||||
|
||||
func (i *jsIPN) fetch(url string) js.Value {
|
||||
return makePromise(func() (any, error) {
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: i.dialer.UserDial,
|
||||
},
|
||||
}
|
||||
res, err := c.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": res.StatusCode,
|
||||
"statusText": res.Status,
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
return makePromise(func() (any, error) {
|
||||
defer res.Body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(res.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.String(), nil
|
||||
})
|
||||
}),
|
||||
// TODO: populate a more complete JS Response object
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type termWriter struct {
|
||||
f js.Value
|
||||
}
|
||||
@@ -367,7 +495,7 @@ type jsNetMapNode struct {
|
||||
|
||||
type jsNetMapSelfNode struct {
|
||||
jsNetMapNode
|
||||
MachineStatus int `json:"machineStatus"`
|
||||
MachineStatus string `json:"machineStatus"`
|
||||
}
|
||||
|
||||
type jsNetMapPeerNode struct {
|
||||
@@ -428,3 +556,61 @@ func generateHostname() string {
|
||||
scale := scales[rand.Intn(len(scales))]
|
||||
return fmt.Sprintf("%s-%s", tail, scale)
|
||||
}
|
||||
|
||||
// makePromise handles the boilerplate of wrapping goroutines with JS promises.
|
||||
// f is run on a goroutine and its return value is used to resolve the promise
|
||||
// (or reject it if an error is returned).
|
||||
func makePromise(f func() (any, error)) js.Value {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
resolve := args[0]
|
||||
reject := args[1]
|
||||
go func() {
|
||||
if res, err := f(); err == nil {
|
||||
resolve.Invoke(res)
|
||||
} else {
|
||||
reject.Invoke(err.Error())
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
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,6 +130,15 @@ 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"
|
||||
@@ -181,6 +190,14 @@ 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"
|
||||
@@ -191,6 +208,11 @@ 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"
|
||||
@@ -234,7 +256,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.1, get-caller-file@^2.0.5:
|
||||
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==
|
||||
@@ -443,6 +465,11 @@ 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"
|
||||
@@ -518,7 +545,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.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
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==
|
||||
@@ -574,7 +601,7 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
typescript@^4.7.4:
|
||||
typescript@>=3.0.1, typescript@^4.7.4:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
@@ -598,11 +625,25 @@ 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"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xterm-addon-fit@^0.5.0:
|
||||
version "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@^4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
|
||||
@@ -613,6 +654,11 @@ y18n@^4.0.0:
|
||||
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"
|
||||
@@ -626,6 +672,11 @@ 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"
|
||||
@@ -642,3 +693,16 @@ 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
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone --clone-only-type=OnlyGetClone
|
||||
|
||||
type StructWithoutPtrs struct {
|
||||
Int int
|
||||
@@ -20,16 +20,18 @@ type StructWithoutPtrs struct {
|
||||
type Map struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructPtrWithPtr map[string]*StructWithPtrs
|
||||
StructPtrWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructWithoutPtr map[string]StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"`
|
||||
|
||||
// Unsupported.
|
||||
// Unsupported views.
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int `json:"-"`
|
||||
StructWithPtrKey map[StructWithPtrs]int `json:"-"`
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}
|
||||
|
||||
type StructWithPtrs struct {
|
||||
@@ -56,3 +58,7 @@ type StructWithSlices struct {
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type OnlyGetClone struct {
|
||||
SinViewerPorFavor bool
|
||||
}
|
||||
|
||||
@@ -73,16 +73,22 @@ func (src *Map) Clone() *Map {
|
||||
dst.SliceInt[k] = append([]int{}, src.SliceInt[k]...)
|
||||
}
|
||||
}
|
||||
if dst.StructWithPtr != nil {
|
||||
dst.StructWithPtr = map[string]*StructWithPtrs{}
|
||||
for k, v := range src.StructWithPtr {
|
||||
dst.StructWithPtr[k] = v.Clone()
|
||||
if dst.StructPtrWithPtr != nil {
|
||||
dst.StructPtrWithPtr = map[string]*StructWithPtrs{}
|
||||
for k, v := range src.StructPtrWithPtr {
|
||||
dst.StructPtrWithPtr[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
if dst.StructPtrWithoutPtr != nil {
|
||||
dst.StructPtrWithoutPtr = map[string]*StructWithoutPtrs{}
|
||||
for k, v := range src.StructPtrWithoutPtr {
|
||||
dst.StructPtrWithoutPtr[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
if dst.StructWithoutPtr != nil {
|
||||
dst.StructWithoutPtr = map[string]*StructWithoutPtrs{}
|
||||
dst.StructWithoutPtr = map[string]StructWithoutPtrs{}
|
||||
for k, v := range src.StructWithoutPtr {
|
||||
dst.StructWithoutPtr[k] = v.Clone()
|
||||
dst.StructWithoutPtr[k] = v
|
||||
}
|
||||
}
|
||||
if dst.SlicesWithPtrs != nil {
|
||||
@@ -121,6 +127,13 @@ func (src *Map) Clone() *Map {
|
||||
dst.StructWithPtrKey[k] = v
|
||||
}
|
||||
}
|
||||
if dst.StructWithPtr != nil {
|
||||
dst.StructWithPtr = map[string]StructWithPtrs{}
|
||||
for k, v := range src.StructWithPtr {
|
||||
v2 := v.Clone()
|
||||
dst.StructWithPtr[k] = *v2
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -128,14 +141,16 @@ func (src *Map) Clone() *Map {
|
||||
var _MapCloneNeedsRegeneration = Map(struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructPtrWithPtr map[string]*StructWithPtrs
|
||||
StructPtrWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructWithoutPtr map[string]StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of StructWithSlices.
|
||||
@@ -181,3 +196,19 @@ 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
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone
|
||||
|
||||
// View returns a readonly view of StructWithPtrs.
|
||||
func (p *StructWithPtrs) View() StructWithPtrsView {
|
||||
@@ -196,18 +196,22 @@ func (v MapView) SliceInt() views.MapFn[string, []int, views.Slice[int]] {
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithPtr, func(t *StructWithPtrs) StructWithPtrsView {
|
||||
func (v MapView) StructPtrWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructPtrWithPtr, func(t *StructWithPtrs) StructWithPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView {
|
||||
func (v MapView) StructPtrWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructPtrWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithoutPtr() views.Map[string, StructWithoutPtrs] {
|
||||
return views.MapOf(v.ж.StructWithoutPtr)
|
||||
}
|
||||
|
||||
func (v MapView) SlicesWithPtrs() views.MapFn[string, []*StructWithPtrs, views.SliceView[*StructWithPtrs, StructWithPtrsView]] {
|
||||
return views.MapFnOf(v.ж.SlicesWithPtrs, func(t []*StructWithPtrs) views.SliceView[*StructWithPtrs, StructWithPtrsView] {
|
||||
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](t)
|
||||
@@ -227,18 +231,26 @@ func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported"
|
||||
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
|
||||
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
|
||||
|
||||
func (v MapView) StructWithPtr() views.MapFn[string, StructWithPtrs, StructWithPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithPtr, func(t StructWithPtrs) StructWithPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _MapViewNeedsRegeneration = Map(struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructPtrWithPtr map[string]*StructWithPtrs
|
||||
StructPtrWithoutPtr map[string]*StructWithoutPtrs
|
||||
StructWithoutPtr map[string]StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithSlices.
|
||||
|
||||
@@ -224,6 +224,18 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
mElem := m.Elem()
|
||||
var template string
|
||||
switch u := mElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
strucT := u
|
||||
args.FieldType = it.QualifiedName(fieldType)
|
||||
if codegen.ContainsPointers(strucT) {
|
||||
args.MapFn = "t.View()"
|
||||
template = "mapFnField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
args.MapValueView = args.MapValueType + "View"
|
||||
} else {
|
||||
template = "mapField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
}
|
||||
case *types.Basic:
|
||||
template = "mapField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
@@ -315,6 +327,8 @@ 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() {
|
||||
@@ -341,11 +355,18 @@ 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`, strings.Join(flagArgs, " "))
|
||||
fmt.Fprintln(buf)
|
||||
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,6 +5,7 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -23,7 +25,6 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -41,6 +42,7 @@ 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"
|
||||
@@ -67,11 +69,13 @@ type Direct struct {
|
||||
linkMon *monitor.Mon // or nil
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
getNLPublicKey func() (key.NLPublic, error) // or nil
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
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
|
||||
@@ -107,6 +111,11 @@ 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.
|
||||
GetNLPublicKey func() (key.NLPublic, error)
|
||||
|
||||
// Status is called when there's a change in status.
|
||||
Status func(Status)
|
||||
@@ -190,6 +199,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
getNLPublicKey: opts.GetNLPublicKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -204,6 +214,7 @@ 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 {
|
||||
@@ -424,6 +435,14 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
oldNodeKey = persist.OldPrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
var nlPub key.NLPublic
|
||||
if c.getNLPublicKey != nil {
|
||||
nlPub, err = c.getNLPublicKey()
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("get nl key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tryingNewKey.IsZero() {
|
||||
if opt.Logout {
|
||||
return false, "", errors.New("no nodekey to log out")
|
||||
@@ -439,6 +458,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
Version: 1,
|
||||
OldNodeKey: oldNodeKey,
|
||||
NodeKey: tryingNewKey.Public(),
|
||||
NLKey: nlPub,
|
||||
Hostinfo: hi,
|
||||
Followup: opt.URL,
|
||||
Timestamp: &now,
|
||||
@@ -695,7 +715,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 Debug.NetMap {
|
||||
if DevKnob.DumpNetMaps {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
@@ -870,7 +890,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 answerPing(c.logf, c.httpc, pr, c.pinger)
|
||||
go c.answerPing(pr)
|
||||
}
|
||||
if u := resp.PopBrowserURL; u != "" && u != sess.lastPopBrowserURL {
|
||||
sess.lastPopBrowserURL = u
|
||||
@@ -924,8 +944,6 @@ 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
|
||||
@@ -939,12 +957,17 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
return errors.New("MapResponse lacked node")
|
||||
}
|
||||
|
||||
if Debug.StripEndpoints {
|
||||
if d := nm.Debug; d != nil {
|
||||
controlUseDERPRoute.Store(d.DERPRoute)
|
||||
controlTrimWGConfig.Store(d.TrimWGConfig)
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
}
|
||||
}
|
||||
if Debug.StripCaps {
|
||||
if DevKnob.StripCaps {
|
||||
nm.SelfNode.Capabilities = nil
|
||||
}
|
||||
|
||||
@@ -1110,25 +1133,23 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// DevKnob contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
var Debug = initDebug()
|
||||
var DevKnob = initDevKnob()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
Disco bool
|
||||
type devKnobs struct {
|
||||
DumpNetMaps bool
|
||||
ForceProxyDNS bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps bool // strip all local node's control-provided capabilities
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
return debug{
|
||||
NetMap: envknob.Bool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
|
||||
func initDevKnob() devKnobs {
|
||||
return devKnobs{
|
||||
DumpNetMaps: envknob.Bool("TS_DEBUG_NETMAP"),
|
||||
ForceProxyDNS: 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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,29 +1157,20 @@ var clockNow = time.Now
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute atomic.Value
|
||||
controlTrimWGConfig atomic.Value
|
||||
controlUseDERPRoute syncs.AtomicValue[opt.Bool]
|
||||
controlTrimWGConfig syncs.AtomicValue[opt.Bool]
|
||||
)
|
||||
|
||||
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 {
|
||||
v, _ := controlUseDERPRoute.Load().(opt.Bool)
|
||||
return v
|
||||
return controlUseDERPRoute.Load()
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
return controlTrimWGConfig.Load()
|
||||
}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
@@ -1196,21 +1208,39 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) {
|
||||
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
|
||||
}
|
||||
if pr.URL == "" {
|
||||
logf("invalid PingRequest with no URL")
|
||||
c.logf("invalid PingRequest with no URL")
|
||||
return
|
||||
}
|
||||
if pr.Types == "" {
|
||||
answerHeadPing(logf, c, pr)
|
||||
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)
|
||||
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(logf, c, pr, pinger, pt)
|
||||
go doPingerPing(c.logf, httpc, pr, c.pinger, pt)
|
||||
default:
|
||||
logf("unsupported ping request type: %q", t)
|
||||
c.logf("unsupported ping request type: %q", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1237,6 +1267,54 @@ 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 {
|
||||
@@ -1313,7 +1391,7 @@ 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.serverHost, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user