Compare commits
298 Commits
bradfitz/a
...
josh/tsweb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b89db4dff | ||
|
|
1dc4151f8b | ||
|
|
8d6cf14456 | ||
|
|
b4947be0c8 | ||
|
|
01e8a152f7 | ||
|
|
2448c000b3 | ||
|
|
903988b392 | ||
|
|
8267ea0f80 | ||
|
|
8fe503057d | ||
|
|
5d9ab502f3 | ||
|
|
a19c110dd3 | ||
|
|
2db6cd1025 | ||
|
|
be9d564c29 | ||
|
|
3a94ece30c | ||
|
|
86a902b201 | ||
|
|
adda2d2a51 | ||
|
|
a80cef0c13 | ||
|
|
84046d6f7c | ||
|
|
ec62217f52 | ||
|
|
21358cf2f5 | ||
|
|
37e7a387ff | ||
|
|
15599323a1 | ||
|
|
60abeb027b | ||
|
|
b9c92b90db | ||
|
|
e206a3663f | ||
|
|
0173a50bf0 | ||
|
|
dbea8217ac | ||
|
|
82cd98609f | ||
|
|
39d173e5fc | ||
|
|
c8551c8a67 | ||
|
|
3a74f2d2d7 | ||
|
|
24c9dbd129 | ||
|
|
62db629227 | ||
|
|
3c481d6b18 | ||
|
|
b3d268c5a1 | ||
|
|
df8f02db3f | ||
|
|
16652ae52c | ||
|
|
aaba49ca10 | ||
|
|
e64cecac8e | ||
|
|
2a67beaacf | ||
|
|
0626cf4183 | ||
|
|
d7962e3bcf | ||
|
|
6eed2811b2 | ||
|
|
e3dccfd7ff | ||
|
|
fa612c28cf | ||
|
|
e5cd765e00 | ||
|
|
bd90781b34 | ||
|
|
e45d51b060 | ||
|
|
730aa1c89c | ||
|
|
f5ec916214 | ||
|
|
69392411d9 | ||
|
|
02bdc654d5 | ||
|
|
70d71ba1e7 | ||
|
|
1af26222b6 | ||
|
|
857cd6c0d7 | ||
|
|
ae525a7394 | ||
|
|
7a18fe3dca | ||
|
|
c2059d5b8a | ||
|
|
ca774c3249 | ||
|
|
508f332bb2 | ||
|
|
f31546809f | ||
|
|
f3c0023add | ||
|
|
41fd4eab5c | ||
|
|
6feb8f4c51 | ||
|
|
ff3442d92d | ||
|
|
0ada42684b | ||
|
|
7ba874d7f1 | ||
|
|
92dfaf53bb | ||
|
|
411c6c316c | ||
|
|
c64af5e676 | ||
|
|
de4696da10 | ||
|
|
390490e7b1 | ||
|
|
3e50a265be | ||
|
|
185825df11 | ||
|
|
790e41645b | ||
|
|
166fe3fb12 | ||
|
|
6be48dfcc6 | ||
|
|
96f008cf87 | ||
|
|
d5a7eabcd0 | ||
|
|
6cd180746f | ||
|
|
02461ea459 | ||
|
|
8cf1af8a07 | ||
|
|
463b3e8f62 | ||
|
|
a9da6b73a8 | ||
|
|
9fe5ece833 | ||
|
|
5404a0557b | ||
|
|
5a317d312d | ||
|
|
c6c39930cc | ||
|
|
a076aaecc6 | ||
|
|
27da7fd5cb | ||
|
|
a7da236d3d | ||
|
|
a93937abc3 | ||
|
|
26d4ccb816 | ||
|
|
9e8a432146 | ||
|
|
24a04d07d1 | ||
|
|
51bc9a6d9d | ||
|
|
e6626366a2 | ||
|
|
8df3fa4638 | ||
|
|
66f6efa8cb | ||
|
|
189f359609 | ||
|
|
b8ad90c2bf | ||
|
|
b1b0fd119b | ||
|
|
1dc1c8b709 | ||
|
|
408522ddad | ||
|
|
1ffc21ad71 | ||
|
|
dee0833b27 | ||
|
|
b03170b901 | ||
|
|
c5243562d7 | ||
|
|
1a4e8da084 | ||
|
|
138662e248 | ||
|
|
1b426cc232 | ||
|
|
8d0ed1c9ba | ||
|
|
e68d87eb44 | ||
|
|
2cfc96aa90 | ||
|
|
addda5b96f | ||
|
|
64c2657448 | ||
|
|
3690bfecb0 | ||
|
|
28bf53f502 | ||
|
|
c8b63a409e | ||
|
|
a201b89e4a | ||
|
|
506c727e30 | ||
|
|
e2d9c99e5b | ||
|
|
01a9906bf8 | ||
|
|
2aeb93003f | ||
|
|
2513d2d728 | ||
|
|
dd45bba76b | ||
|
|
ebdd25920e | ||
|
|
431329e47c | ||
|
|
7d9b1de3aa | ||
|
|
2c94e3c4ad | ||
|
|
04c2c5bd80 | ||
|
|
96cab21383 | ||
|
|
63d9c7b9b3 | ||
|
|
b09000ad5d | ||
|
|
eb26c081b1 | ||
|
|
44937b59e7 | ||
|
|
535b925d1b | ||
|
|
434af15a04 | ||
|
|
bc537adb1a | ||
|
|
0aa4c6f147 | ||
|
|
ae319b4636 | ||
|
|
c7f5bc0f69 | ||
|
|
81bc812402 | ||
|
|
0848b36dd2 | ||
|
|
39f22a357d | ||
|
|
394c9de02b | ||
|
|
c7052154d5 | ||
|
|
3dedcd1640 | ||
|
|
5a9914a92f | ||
|
|
66164b9307 | ||
|
|
40e2b312b6 | ||
|
|
689426d6bc | ||
|
|
add6dc8ccc | ||
|
|
894693f352 | ||
|
|
4512e213d5 | ||
|
|
8f43ddf1a2 | ||
|
|
681d4897cc | ||
|
|
93ae11105d | ||
|
|
84a1106fa7 | ||
|
|
aac974a5e5 | ||
|
|
6590fc3a94 | ||
|
|
486059589b | ||
|
|
59f4f33f60 | ||
|
|
ac8e69b713 | ||
|
|
0f3b55c299 | ||
|
|
4691e012a9 | ||
|
|
e133bb570b | ||
|
|
adc97e9c4d | ||
|
|
d24a8f7b5a | ||
|
|
8dbda1a722 | ||
|
|
cced414c7d | ||
|
|
cab5c46481 | ||
|
|
63cd581c3f | ||
|
|
a5235e165c | ||
|
|
c8829b742b | ||
|
|
39ffa16853 | ||
|
|
b59e7669c1 | ||
|
|
21741e111b | ||
|
|
7b9c7bc42b | ||
|
|
affc4530a2 | ||
|
|
485bcdc951 | ||
|
|
878a20df29 | ||
|
|
a28d280b95 | ||
|
|
9f867ad2c5 | ||
|
|
c0701b130d | ||
|
|
656809e4ee | ||
|
|
e34ba3223c | ||
|
|
c18dc57861 | ||
|
|
ffb16cdffb | ||
|
|
d3d503d997 | ||
|
|
abc00e9c8d | ||
|
|
190b7a4cca | ||
|
|
0d8ef1ff35 | ||
|
|
329751c48e | ||
|
|
9ddef8cdbf | ||
|
|
9140f193bc | ||
|
|
05c1be3e47 | ||
|
|
e6e63c2305 | ||
|
|
c0984f88dc | ||
|
|
eeccbccd08 | ||
|
|
69de3bf7bf | ||
|
|
1813c2a162 | ||
|
|
0a9932f3b2 | ||
|
|
9c5c9d0a50 | ||
|
|
9f6249b26d | ||
|
|
de635ac0a8 | ||
|
|
003089820d | ||
|
|
03a323de4e | ||
|
|
a8f60cf6e8 | ||
|
|
f91481075d | ||
|
|
adc5997592 | ||
|
|
768baafcb5 | ||
|
|
43983a4a3b | ||
|
|
44d0c1ab06 | ||
|
|
8775c646be | ||
|
|
ad3d6e31f0 | ||
|
|
25eab78573 | ||
|
|
c7fb26acdb | ||
|
|
c37af58ea4 | ||
|
|
bf1d69f25b | ||
|
|
2075c39fd7 | ||
|
|
49a9e62d58 | ||
|
|
56c72d9cde | ||
|
|
d5405c66b7 | ||
|
|
3ae6f898cf | ||
|
|
16abd7e07c | ||
|
|
2a95ee4680 | ||
|
|
deb2f5e793 | ||
|
|
f93cf6fa03 | ||
|
|
b800663779 | ||
|
|
124363e0ca | ||
|
|
e16cb523aa | ||
|
|
a8cc519c70 | ||
|
|
fddf43f3d1 | ||
|
|
9787ec6f4a | ||
|
|
40f11c50a1 | ||
|
|
38d90fa330 | ||
|
|
999814e9e1 | ||
|
|
bb91cfeae7 | ||
|
|
3181bbb8e4 | ||
|
|
46a9782322 | ||
|
|
d89c61b812 | ||
|
|
341e1af873 | ||
|
|
b811a316bc | ||
|
|
6e584ffa33 | ||
|
|
a54d13294f | ||
|
|
135580a5a8 | ||
|
|
d9c21936c3 | ||
|
|
1e8b4e770a | ||
|
|
105c545366 | ||
|
|
c2efe46f72 | ||
|
|
ff9727c9ff | ||
|
|
f8cef1ba08 | ||
|
|
6dc6ea9b37 | ||
|
|
78b0bd2957 | ||
|
|
097602b3ca | ||
|
|
db800ddeac | ||
|
|
33c541ae30 | ||
|
|
e121c2f724 | ||
|
|
25525b7754 | ||
|
|
9bb91cb977 | ||
|
|
259163dfe1 | ||
|
|
f56a7559ce | ||
|
|
d10cefdb9b | ||
|
|
9f00510833 | ||
|
|
955aa188b3 | ||
|
|
73beaaf360 | ||
|
|
b0d543f7a1 | ||
|
|
73beaf59fb | ||
|
|
a3b709f0c4 | ||
|
|
283ae702c1 | ||
|
|
6fd6fe11f2 | ||
|
|
027b46d0c1 | ||
|
|
0de1b74fbb | ||
|
|
ad5e04249b | ||
|
|
60510a6ae7 | ||
|
|
1ea270375a | ||
|
|
ca1b3fe235 | ||
|
|
9a217ec841 | ||
|
|
9feb483ad3 | ||
|
|
7d8feb2784 | ||
|
|
1a629a4715 | ||
|
|
e8db43e8fa | ||
|
|
937e96f43d | ||
|
|
f76a8d93da | ||
|
|
2ea765e5d8 | ||
|
|
def659d1ec | ||
|
|
946dfec98a | ||
|
|
9259377a7f | ||
|
|
88b8a09d37 | ||
|
|
6c82cebe57 | ||
|
|
4ef3fed100 | ||
|
|
cf9169e4be | ||
|
|
0350cf0438 | ||
|
|
5294125e7a | ||
|
|
758c37b83d | ||
|
|
85184a58ed | ||
|
|
9fc4e876e3 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
go.mod filter=go-mod
|
||||
*.go diff=golang
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Support requests and Troubleshooting
|
||||
- name: Support
|
||||
url: https://tailscale.com/contact/support/
|
||||
about: Contact us for support
|
||||
- name: Troubleshooting
|
||||
url: https://tailscale.com/kb/1023/troubleshooting
|
||||
about: Troubleshoot common issues. Contact us by email at support@tailscale.com.
|
||||
about: Troubleshoot common issues
|
||||
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -7,14 +7,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Please check if your feature request is [already filed](https://github.com/tailscale/tailscale/issues).
|
||||
- type: input
|
||||
id: request
|
||||
attributes:
|
||||
label: Tell us about your idea!
|
||||
description: What is your feature request?
|
||||
placeholder: e.g., A pet pangolin
|
||||
validations:
|
||||
required: true
|
||||
Tell us about your idea!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -2,15 +2,20 @@
|
||||
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "go.mod:"
|
||||
## Disabled between releases. We reenable it briefly after every
|
||||
## stable release, pull in all changes, and close it again so that
|
||||
## the tree remains more stable during development and the upstream
|
||||
## changes have time to soak before the next release.
|
||||
# - package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
# commit-message:
|
||||
# prefix: "go.mod:"
|
||||
# open-pull-requests-limit: 100
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: ".github:"
|
||||
|
||||
26
.github/workflows/cifuzz.yml
vendored
Normal file
26
.github/workflows/cifuzz.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CIFuzz
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'tailscale'
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Run Fuzzers
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'tailscale'
|
||||
fuzz-seconds: 300
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
8
.github/workflows/cross-darwin.yml
vendored
8
.github/workflows/cross-darwin.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
@@ -37,6 +37,12 @@ jobs:
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- name: iOS build most
|
||||
env:
|
||||
GOOS: ios
|
||||
GOARCH: arm64
|
||||
run: go install ./ipn/... ./wgengine/ ./types/... ./control/controlclient
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/depaware.yml
vendored
2
.github/workflows/depaware.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
2
.github/workflows/go_generate.yml
vendored
2
.github/workflows/go_generate.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
17
.github/workflows/linux-race.yml
vendored
17
.github/workflows/linux-race.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
@@ -31,6 +31,21 @@ jobs:
|
||||
- name: Run tests and benchmarks with -race flag on linux
|
||||
run: go test -race -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
17
.github/workflows/linux.yml
vendored
17
.github/workflows/linux.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
@@ -40,6 +40,21 @@ jobs:
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
17
.github/workflows/linux32.yml
vendored
17
.github/workflows/linux32.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
@@ -31,6 +31,21 @@ jobs:
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
- name: Check that no files have been added to the repo
|
||||
run: |
|
||||
# Note: The "error: pathspec..." you see below is normal!
|
||||
# In the success case in which there are no new untracked files,
|
||||
# git ls-files complains about the pathspec not matching anything.
|
||||
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
||||
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
||||
then
|
||||
echo "Build/test created untracked files in the repo (file names above)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
2
.github/workflows/staticcheck.yml
vendored
2
.github/workflows/staticcheck.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
6
.github/workflows/vm.yml
vendored
6
.github/workflows/vm.yml
vendored
@@ -12,11 +12,13 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Set GOPATH
|
||||
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/windows-race.yml
vendored
2
.github/workflows/windows-race.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.spk
|
||||
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
@@ -50,12 +50,11 @@ ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
|
||||
RUN GOARCH=$TARGETARCH go install -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled
|
||||
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
FROM ghcr.io/tailscale/alpine-base:3.14
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
6
Dockerfile.base
Normal file
6
Dockerfile.base
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
FROM alpine:3.15
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
17
Makefile
17
Makefile
@@ -1,4 +1,6 @@
|
||||
IMAGE_REPO ?= tailscale/tailscale
|
||||
SYNO_ARCH ?= "amd64"
|
||||
SYNO_DSM ?= "7"
|
||||
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
@@ -23,11 +25,22 @@ build386:
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
|
||||
buildmultiarchimage:
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t ${IMAGE_REPO}:latest --push -f Dockerfile .
|
||||
./build_docker.sh
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
spk:
|
||||
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
|
||||
|
||||
spkall:
|
||||
mkdir -p spks
|
||||
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
|
||||
|
||||
pushspk: spk
|
||||
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
|
||||
scp tailscale.spk root@${SYNO_HOST}:
|
||||
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
|
||||
|
||||
@@ -8,11 +8,12 @@ Private WireGuard® networks made easy
|
||||
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
|
||||
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
|
||||
The Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.17.0
|
||||
1.21.0
|
||||
|
||||
224
api.md
224
api.md
@@ -15,6 +15,10 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST device routes](#device-routes-post)
|
||||
- Authorize machine
|
||||
- [POST device authorized](#device-authorized-post)
|
||||
- Tags
|
||||
- [POST device tags](#device-tags-post)
|
||||
- Key
|
||||
- [POST device key](#device-key-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
@@ -23,6 +27,11 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [Keys](#tailnet-keys)
|
||||
- [GET tailnet keys](#tailnet-keys-get)
|
||||
- [POST tailnet key](#tailnet-keys-post)
|
||||
- [GET tailnet key](#tailnet-keys-key-get)
|
||||
- [DELETE tailnet key](#tailnet-keys-key-delete)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
@@ -263,6 +272,68 @@ curl 'https://api.tailscale.com/api/v2/device/11055/authorized' \
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-tags-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/tags` - update tags on a device
|
||||
|
||||
Updates the tags set on a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`tags` - The new list of tags for the device.
|
||||
|
||||
```
|
||||
{
|
||||
"tags": ["tag:foo", "tag:bar"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/tags' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"tags": ["tag:foo", "tag:bar"]}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-key-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/key` - update device key
|
||||
|
||||
Allows for updating properties on the device key.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`keyExpiryDisabled`
|
||||
|
||||
- Provide `true` to disable the device's key expiry. The original key expiry time is still maintained. Upon re-enabling, the key will expire at that original time.
|
||||
- Provide `false` to enable the device's key expiry. Sets the key to expire at the original expiry time prior to disabling. The key may already have expired. In that case, the device must be re-authenticated.
|
||||
- Empty value will not change the key expiry.
|
||||
|
||||
```
|
||||
{
|
||||
"keyExpiryDisabled": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/key' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"keyExpiryDisabled": true}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
## Tailnet
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
@@ -670,6 +741,159 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys></a>
|
||||
|
||||
### Keys
|
||||
|
||||
<a name=tailnet-keys-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys` - list the keys for a tailnet
|
||||
|
||||
Returns a list of active keys for a tailnet
|
||||
for the user who owns the API key used to perform this query.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the IDs of all active keys.
|
||||
This includes both API keys and also machine authentication keys.
|
||||
In the future, this may provide more information about each key than just the ID.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{"keys": [
|
||||
{"id": "kYKVU14CNTRL"},
|
||||
{"id": "k68VdZ3CNTRL"},
|
||||
{"id": "kJ9nq43CNTRL"},
|
||||
{"id": "kkThgj1CNTRL"}
|
||||
]}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/keys` - create a new key for a tailnet
|
||||
|
||||
Create a new key in a tailnet associated
|
||||
with the user who owns the API key used to perform this request.
|
||||
Supply the tailnet in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the provided capabilities in addition to the
|
||||
generated key. The key should be recorded and kept safe and secure as it
|
||||
wields the capabilities specified in the request. The identity of the key
|
||||
is embedded in the key itself and can be used to perform operations on
|
||||
the key (e.g., revoking it or retrieving information about it).
|
||||
The full key can no longer be retrieved by the server.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
echo '{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | curl -X POST --data-binary @- https://api.tailscale.com/api/v2/tailnet/example.com/keys \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Content-Type: application/json" | jsonfmt
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"created": "2021-12-09T23:22:39Z",
|
||||
"expires": "2022-03-09T23:22:39Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys/:keyid` - get information for a specific key
|
||||
|
||||
Returns a JSON object with information about specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with information about the key such as
|
||||
when it was created and when it expires.
|
||||
It also lists the capabilities associated with the key.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"created": "2021-12-09T22:13:53Z",
|
||||
"expires": "2022-03-09T22:13:53Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/tailnet/:tailnet/keys/:keyid` - delete a specific key
|
||||
|
||||
Deletes a specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
This reports status 200 upon success.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
@@ -30,12 +30,14 @@ else
|
||||
fi
|
||||
|
||||
long_suffix="$change_suffix-t$short_hash"
|
||||
SHORT="$major.$minor.$patch"
|
||||
MINOR="$major.$minor"
|
||||
SHORT="$MINOR.$patch"
|
||||
LONG="${SHORT}$long_suffix"
|
||||
GIT_HASH="$git_hash"
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
VERSION_MINOR="$MINOR"
|
||||
VERSION_SHORT="$SHORT"
|
||||
VERSION_LONG="$LONG"
|
||||
VERSION_GIT_HASH="$GIT_HASH"
|
||||
@@ -43,4 +45,4 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
exec ./tool/go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
|
||||
@@ -19,10 +19,28 @@
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./build_dist.sh shellvars)
|
||||
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
|
||||
export PATH=$PWD/tool:$PATH
|
||||
|
||||
docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:$VERSION_SHORT -t tailscale:latest .
|
||||
eval $(./build_dist.sh shellvars)
|
||||
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
|
||||
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
|
||||
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
|
||||
|
||||
PUSH="${PUSH:-false}"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
TAGS="${TAGS:-${DEFAULT_TAGS}}"
|
||||
BASE="${BASE:-${DEFAULT_BASE}}"
|
||||
|
||||
go run github.com/tailscale/mkctr@latest \
|
||||
--gopaths="\
|
||||
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
|
||||
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
|
||||
--ldflags="\
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}"
|
||||
|
||||
@@ -19,9 +19,9 @@ func New(socket string) (*BIRDClient, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
b := &BIRDClient{socket: socket, conn: conn, bs: bufio.NewScanner(conn)}
|
||||
b := &BIRDClient{socket: socket, conn: conn, scanner: bufio.NewScanner(conn)}
|
||||
// Read and discard the first line as that is the welcome message.
|
||||
if _, err := b.readLine(); err != nil {
|
||||
if _, err := b.readResponse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
@@ -29,9 +29,9 @@ func New(socket string) (*BIRDClient, error) {
|
||||
|
||||
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
|
||||
type BIRDClient struct {
|
||||
socket string
|
||||
conn net.Conn
|
||||
bs *bufio.Scanner
|
||||
socket string
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
// Close closes the underlying connection to BIRD.
|
||||
@@ -39,7 +39,7 @@ func (b *BIRDClient) Close() error { return b.conn.Close() }
|
||||
|
||||
// DisableProtocol disables the provided protocol.
|
||||
func (b *BIRDClient) DisableProtocol(protocol string) error {
|
||||
out, err := b.exec("disable %s\n", protocol)
|
||||
out, err := b.exec("disable %s", protocol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (b *BIRDClient) DisableProtocol(protocol string) error {
|
||||
|
||||
// EnableProtocol enables the provided protocol.
|
||||
func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
out, err := b.exec("enable %s\n", protocol)
|
||||
out, err := b.exec("enable %s", protocol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,19 +65,65 @@ func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
return fmt.Errorf("failed to enable %s: %v", protocol, out)
|
||||
}
|
||||
|
||||
// BIRD CLI docs from https://bird.network.cz/?get_doc&v=20&f=prog-2.html#ss2.9
|
||||
|
||||
// Each session of the CLI consists of a sequence of request and replies,
|
||||
// slightly resembling the FTP and SMTP protocols.
|
||||
// Requests are commands encoded as a single line of text,
|
||||
// replies are sequences of lines starting with a four-digit code
|
||||
// followed by either a space (if it's the last line of the reply) or
|
||||
// a minus sign (when the reply is going to continue with the next line),
|
||||
// the rest of the line contains a textual message semantics of which depends on the numeric code.
|
||||
// If a reply line has the same code as the previous one and it's a continuation line,
|
||||
// the whole prefix can be replaced by a single white space character.
|
||||
//
|
||||
// Reply codes starting with 0 stand for ‘action successfully completed’ messages,
|
||||
// 1 means ‘table entry’, 8 ‘runtime error’ and 9 ‘syntax error’.
|
||||
|
||||
func (b *BIRDClient) exec(cmd string, args ...interface{}) (string, error) {
|
||||
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.readLine()
|
||||
fmt.Fprintln(b.conn)
|
||||
return b.readResponse()
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readLine() (string, error) {
|
||||
if !b.bs.Scan() {
|
||||
return "", fmt.Errorf("reading response from bird failed")
|
||||
// hasResponseCode reports whether the provided byte slice is
|
||||
// prefixed with a BIRD response code.
|
||||
// Equivalent regex: `^\d{4}[ -]`.
|
||||
func hasResponseCode(s []byte) bool {
|
||||
if len(s) < 5 {
|
||||
return false
|
||||
}
|
||||
if err := b.bs.Err(); err != nil {
|
||||
return "", err
|
||||
for _, b := range s[:4] {
|
||||
if '0' <= b && b <= '9' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return b.bs.Text(), nil
|
||||
return s[4] == ' ' || s[4] == '-'
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readResponse() (string, error) {
|
||||
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
|
||||
}
|
||||
out := b.scanner.Bytes()
|
||||
if _, err := resp.Write(out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasResponseCode(out) {
|
||||
done = out[4] == ' '
|
||||
}
|
||||
if !done {
|
||||
resp.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
return resp.String(), nil
|
||||
}
|
||||
|
||||
111
chirp/chirp_test.go
Normal file
111
chirp/chirp_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 chirp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeBIRD struct {
|
||||
net.Listener
|
||||
protocolsEnabled map[string]bool
|
||||
sock string
|
||||
}
|
||||
|
||||
func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD {
|
||||
sock := filepath.Join(t.TempDir(), "sock")
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pe := make(map[string]bool)
|
||||
for _, p := range protocols {
|
||||
pe[p] = false
|
||||
}
|
||||
return &fakeBIRD{
|
||||
Listener: l,
|
||||
protocolsEnabled: pe,
|
||||
sock: sock,
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *fakeBIRD) listen() error {
|
||||
for {
|
||||
c, err := fb.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
go fb.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *fakeBIRD) handle(c net.Conn) {
|
||||
fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.")
|
||||
sc := bufio.NewScanner(c)
|
||||
for sc.Scan() {
|
||||
cmd := sc.Text()
|
||||
args := strings.Split(cmd, " ")
|
||||
switch args[0] {
|
||||
case "enable":
|
||||
en, ok := fb.protocolsEnabled[args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
|
||||
} else if en {
|
||||
fmt.Fprintf(c, "0010-%s: already enabled\n", args[1])
|
||||
} else {
|
||||
fmt.Fprintf(c, "0011-%s: enabled\n", args[1])
|
||||
}
|
||||
fmt.Fprintln(c, "0000 ")
|
||||
fb.protocolsEnabled[args[1]] = true
|
||||
case "disable":
|
||||
en, ok := fb.protocolsEnabled[args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
|
||||
} else if !en {
|
||||
fmt.Fprintf(c, "0008-%s: already disabled\n", args[1])
|
||||
} else {
|
||||
fmt.Fprintf(c, "0009-%s: disabled\n", args[1])
|
||||
}
|
||||
fmt.Fprintln(c, "0000 ")
|
||||
fb.protocolsEnabled[args[1]] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChirp(t *testing.T) {
|
||||
fb := newFakeBIRD(t, "tailscale")
|
||||
defer fb.Close()
|
||||
go fb.listen()
|
||||
c, err := New(fb.sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.DisableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.DisableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("rando"); err == nil {
|
||||
t.Fatalf("enabling %q succeded", "rando")
|
||||
}
|
||||
if err := c.DisableProtocol("rando"); err == nil {
|
||||
t.Fatalf("disabling %q succeded", "rando")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ var (
|
||||
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
|
||||
TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
|
||||
TailscaledSocketSetExplicitly bool
|
||||
|
||||
// TailscaledDialer is the DialContext func that connects to the local machine's
|
||||
// tailscaled or equivalent.
|
||||
TailscaledDialer = defaultDialer
|
||||
@@ -47,7 +50,8 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// TODO: make this part of a safesocket.ConnectionStrategy
|
||||
if !TailscaledSocketSetExplicitly {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
@@ -56,7 +60,11 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
|
||||
// The user provided a non-default tailscaled socket address.
|
||||
// Connect only to exactly what they provided.
|
||||
s.UseFallback(false)
|
||||
return safesocket.Connect(s)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -90,6 +98,31 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
return tsClient.Do(req)
|
||||
}
|
||||
|
||||
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
|
||||
res, err := DoLocalRequest(req)
|
||||
if err == nil {
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
|
||||
onVersionMismatch(version.Long, server)
|
||||
}
|
||||
if res.StatusCode == 403 {
|
||||
all, _ := ioutil.ReadAll(res.Body)
|
||||
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
if ue, ok := err.(*url.Error); ok {
|
||||
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
|
||||
path := req.URL.Path
|
||||
pathPrefix := path
|
||||
if i := strings.Index(path, "?"); i != -1 {
|
||||
pathPrefix = path[:i]
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type errorJSON struct {
|
||||
Error string
|
||||
}
|
||||
@@ -140,32 +173,16 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
if ue, ok := err.(*url.Error); ok {
|
||||
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
|
||||
pathPrefix := path
|
||||
if i := strings.Index(path, "?"); i != -1 {
|
||||
pathPrefix = path[:i]
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
|
||||
onVersionMismatch(version.Long, server)
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != wantStatus {
|
||||
if res.StatusCode == 403 {
|
||||
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(slurp))}
|
||||
}
|
||||
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
|
||||
return nil, bestError(err, slurp)
|
||||
}
|
||||
return slurp, nil
|
||||
@@ -223,12 +240,22 @@ func BugReport(ctx context.Context, note string) (string, error) {
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// DebugAction invokes a debug action, such as "rebind" or "restun".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func DebugAction(ctx context.Context, action string) error {
|
||||
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
}
|
||||
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
@@ -267,7 +294,7 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -295,6 +322,30 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
|
||||
return fts, nil
|
||||
}
|
||||
|
||||
// PushFile sends Taildrop file r to target.
|
||||
//
|
||||
// A size of -1 means unknown.
|
||||
// The name parameter is the original filename, not escaped.
|
||||
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size != -1 {
|
||||
req.ContentLength = size
|
||||
}
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
|
||||
}
|
||||
|
||||
func CheckIPForwarding(ctx context.Context) error {
|
||||
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,14 +12,11 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
var dnsCache atomic.Value // of []byte
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
@@ -37,6 +34,7 @@ func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
dnsEntries := make(map[string][]net.IP)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
@@ -47,23 +45,23 @@ func refreshBootstrapDNS() {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
dnsEntries[name] = addrs
|
||||
}
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
return
|
||||
}
|
||||
dnsCache.Store(j)
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
j, _ := dnsCache.Load().([]byte)
|
||||
// Bootstrap DNS requests occur cross-regions,
|
||||
// and are randomized per request,
|
||||
// so keeping a connection open is pointlessly expensive.
|
||||
w.Header().Set("Connection", "close")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
35
cmd/derper/bootstrap_dns_test.go
Normal file
35
cmd/derper/bootstrap_dns_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
prev := *bootstrapDNS
|
||||
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
|
||||
defer func() {
|
||||
*bootstrapDNS = prev
|
||||
}()
|
||||
refreshBootstrapDNS()
|
||||
w := new(bitbucketResponseWriter)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(b *testing.PB) {
|
||||
for b.Next() {
|
||||
handleBootstrapDNS(w, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type bitbucketResponseWriter struct{}
|
||||
|
||||
func (b *bitbucketResponseWriter) Header() http.Header { return make(http.Header) }
|
||||
|
||||
func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}
|
||||
@@ -67,8 +67,8 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load cert: %w", err)
|
||||
}
|
||||
if x509Cert.VerifyHostname(hostname) != nil {
|
||||
return nil, errors.New("refuse to load cert: hostname mismatch with key")
|
||||
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
||||
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
||||
}
|
||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -35,23 +38,30 @@ import (
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
|
||||
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
|
||||
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
|
||||
)
|
||||
|
||||
var (
|
||||
stats = new(metrics.Set)
|
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"}
|
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"}
|
||||
stats = new(metrics.Set)
|
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"}
|
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"}
|
||||
tlsRequestVersion = &metrics.LabelMap{Label: "version"}
|
||||
tlsActiveVersion = &metrics.LabelMap{Label: "version"}
|
||||
|
||||
stunReadError = stunDisposition.Get("read_error")
|
||||
stunNotSTUN = stunDisposition.Get("not_stun")
|
||||
@@ -66,6 +76,8 @@ func init() {
|
||||
stats.Set("counter_requests", stunDisposition)
|
||||
stats.Set("counter_addrfamily", stunAddrFamily)
|
||||
expvar.Publish("stun", stats)
|
||||
expvar.Publish("derper_tls_request_version", tlsRequestVersion)
|
||||
expvar.Publish("gauge_derper_tls_active_version", tlsActiveVersion)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
@@ -235,25 +247,61 @@ func main() {
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, "80"),
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
// and not hit net/http/pprof's "profile
|
||||
// duration exceeds server's WriteTimeout".
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
|
||||
httpsrv.TLSConfig.MinVersion = tls.VersionTLS12
|
||||
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil {
|
||||
label := "unknown"
|
||||
switch r.TLS.Version {
|
||||
case tls.VersionTLS10:
|
||||
label = "1.0"
|
||||
case tls.VersionTLS11:
|
||||
label = "1.1"
|
||||
case tls.VersionTLS12:
|
||||
label = "1.2"
|
||||
case tls.VersionTLS13:
|
||||
label = "1.3"
|
||||
}
|
||||
tlsRequestVersion.Add(label, 1)
|
||||
tlsActiveVersion.Add(label, 1)
|
||||
defer tlsActiveVersion.Add(label, -1)
|
||||
}
|
||||
}()
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
|
||||
// Set HTTP headers to appease automated security scanners.
|
||||
//
|
||||
// Security automation gets cranky when HTTPS sites don't
|
||||
// set HSTS, and when they don't specify a content
|
||||
// security policy for XSS mitigation.
|
||||
//
|
||||
// DERP's HTTP interface is only ever used for debug
|
||||
// access (for which trivial safe policies work just
|
||||
// fine), and by DERP clients which don't obey any of
|
||||
// these browser-centric headers anyway.
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
|
||||
mux.ServeHTTP(w, r)
|
||||
})
|
||||
if *httpPort > -1 {
|
||||
go func() {
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
// and not hit net/http/pprof's "profile
|
||||
// duration exceeds server's WriteTimeout".
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
err = rateLimitedListenAndServeTLS(httpsrv)
|
||||
} else {
|
||||
log.Printf("derper: serving on %s", *addr)
|
||||
err = httpsrv.ListenAndServe()
|
||||
@@ -347,3 +395,63 @@ func defaultMeshPSKFile() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func rateLimitedListenAndServeTLS(srv *http.Server) error {
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rln := newRateLimitedListener(ln, rate.Limit(*acceptConnLimit), *acceptConnBurst)
|
||||
expvar.Publish("tls_listener", rln.ExpVar())
|
||||
defer rln.Close()
|
||||
return srv.ServeTLS(rln, "", "")
|
||||
}
|
||||
|
||||
type rateLimitedListener struct {
|
||||
// These are at the start of the struct to ensure 64-bit alignment
|
||||
// on 32-bit architecture regardless of what other fields may exist
|
||||
// in this package.
|
||||
numAccepts expvar.Int // does not include number of rejects
|
||||
numRejects expvar.Int
|
||||
|
||||
net.Listener
|
||||
|
||||
lim *rate.Limiter
|
||||
}
|
||||
|
||||
func newRateLimitedListener(ln net.Listener, limit rate.Limit, burst int) *rateLimitedListener {
|
||||
return &rateLimitedListener{Listener: ln, lim: rate.NewLimiter(limit, burst)}
|
||||
}
|
||||
|
||||
func (l *rateLimitedListener) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_accepted_connections", &l.numAccepts)
|
||||
m.Set("counter_rejected_connections", &l.numRejects)
|
||||
return m
|
||||
}
|
||||
|
||||
var errLimitedConn = errors.New("cannot accept connection; rate limited")
|
||||
|
||||
func (l *rateLimitedListener) Accept() (net.Conn, error) {
|
||||
// Even under a rate limited situation, we accept the connection immediately
|
||||
// and close it, rather than being slow at accepting new connections.
|
||||
// This provides two benefits: 1) it signals to the client that something
|
||||
// is going on on the server, and 2) it prevents new connections from
|
||||
// piling up and occupying resources in the OS kernel.
|
||||
// The client will retry as needing (with backoffs in place).
|
||||
cn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.lim.Allow() {
|
||||
l.numRejects.Add(1)
|
||||
cn.Close()
|
||||
return nil, errLimitedConn
|
||||
}
|
||||
l.numAccepts.Add(1)
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
@@ -33,11 +35,21 @@ var (
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
)
|
||||
|
||||
// certReissueAfter is the time after which we expect all certs to be
|
||||
// reissued, at minimum.
|
||||
//
|
||||
// This is currently set to the date of the LetsEncrypt ALPN revocation event of Jan 2022:
|
||||
// https://community.letsencrypt.org/t/questions-about-renewing-before-tls-alpn-01-revocations/170449
|
||||
//
|
||||
// If there's another revocation event, bump this again.
|
||||
var certReissueAfter = time.Unix(1643226768, 0)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
state = map[nodePair]pairStatus{}
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastDERPMapAt time.Time
|
||||
certs = map[string]*x509.Certificate{}
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -46,6 +58,12 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
|
||||
}
|
||||
|
||||
func setCert(name string, cert *x509.Certificate) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
certs[name] = cert
|
||||
}
|
||||
|
||||
type overallStatus struct {
|
||||
good, bad []string
|
||||
}
|
||||
@@ -93,6 +111,27 @@ func getOverallStatus() (o overallStatus) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subjs []string
|
||||
for k := range certs {
|
||||
subjs = append(subjs, k)
|
||||
}
|
||||
sort.Strings(subjs)
|
||||
|
||||
soon := time.Now().Add(14 * 24 * time.Hour) // in 2 weeks; autocert does 30 days by default
|
||||
for _, s := range subjs {
|
||||
cert := certs[s]
|
||||
if cert.NotBefore.Before(certReissueAfter) {
|
||||
o.addBadf("cert %q needs reissuing; NotBefore=%v", s, cert.NotBefore.Format(time.RFC3339))
|
||||
continue
|
||||
}
|
||||
if cert.NotAfter.Before(soon) {
|
||||
o.addBadf("cert %q expiring soon (%v); wasn't auto-refreshed", s, cert.NotAfter.Format(time.RFC3339))
|
||||
continue
|
||||
}
|
||||
o.addGoodf("cert %q good %v - %v", s, cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -359,6 +398,21 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs, ok := dc.TLSConnectionState()
|
||||
if !ok {
|
||||
dc.Close()
|
||||
return nil, errors.New("no TLS state")
|
||||
}
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
dc.Close()
|
||||
return nil, errors.New("no peer certificates")
|
||||
}
|
||||
if cs.ServerName != n.HostName {
|
||||
dc.Close()
|
||||
return nil, fmt.Errorf("TLS server name %q != derp hostname %q", cs.ServerName, n.HostName)
|
||||
}
|
||||
setCert(cs.ServerName, cs.PeerCertificates[0])
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
m, err := dc.Recv()
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
// The hello binary runs hello.ts.net.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
@@ -69,11 +72,31 @@ func main() {
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
hs := &http.Server{
|
||||
Addr: *httpsAddr,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
switch hi.ServerName {
|
||||
case "hello.ts.net":
|
||||
return tailscale.GetCertificate(hi)
|
||||
case "hello.ipn.dev":
|
||||
c, err := tls.LoadX509KeyPair(
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
return nil, errors.New("invalid SNI name")
|
||||
},
|
||||
},
|
||||
IdleTimeout: 30 * time.Second,
|
||||
ReadHeaderTimeout: 20 * time.Second,
|
||||
MaxHeaderBytes: 10 << 10,
|
||||
}
|
||||
errc <- hs.ListenAndServeTLS("", "")
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
@@ -127,8 +150,9 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
if strings.Contains(r.Host, "100.101.102.103") ||
|
||||
strings.Contains(r.Host, "hello.ipn.dev") {
|
||||
host = "hello.ts.net"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
@@ -137,6 +161,10 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
|
||||
http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
46
cmd/printdep/printdep.go
Normal file
46
cmd/printdep/printdep.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The printdep command is a build system tool for printing out information
|
||||
// about dependencies.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ts "tailscale.com"
|
||||
)
|
||||
|
||||
var (
|
||||
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
|
||||
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *goToolchain {
|
||||
fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
|
||||
}
|
||||
if *goToolchainURL {
|
||||
var suffix string
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
// None
|
||||
case "arm64":
|
||||
suffix = "-" + runtime.GOARCH
|
||||
default:
|
||||
log.Fatalf("unsupported GOARCH %q", runtime.GOARCH)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
log.Fatalf("unsupported GOOS %q", runtime.GOOS)
|
||||
}
|
||||
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@@ -92,9 +91,6 @@ func runCert(ctx context.Context, args []string) error {
|
||||
certArgs.keyFile = domain + ".key"
|
||||
}
|
||||
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
|
||||
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
|
||||
return fmt.Errorf("%v\n\nUse 'sudo tailscale cert' or 'tailscale up --operator=$USER' to not require root.", err)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,7 +104,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
if version.IsMacSysExt() {
|
||||
dir = "io.tailscale.ipn.macsys"
|
||||
}
|
||||
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s\n", dir)
|
||||
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s/Data\n", dir)
|
||||
}
|
||||
if certArgs.certFile != "" {
|
||||
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var Stderr io.Writer = os.Stderr
|
||||
@@ -155,6 +156,9 @@ change in the future.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -164,8 +168,16 @@ change in the future.
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
rootfs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "socket" {
|
||||
tailscale.TailscaledSocketSetExplicitly = true
|
||||
}
|
||||
})
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
|
||||
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
|
||||
}
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
@@ -191,7 +203,8 @@ var rootArgs struct {
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
@@ -57,6 +59,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
distro distro.Distro
|
||||
|
||||
want string
|
||||
}{
|
||||
@@ -313,6 +316,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
@@ -329,7 +333,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
goos: "openbsd",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
@@ -405,6 +409,21 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeAllowLANAccess: true,
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
|
||||
},
|
||||
{
|
||||
name: "ignore_login_server_synonym",
|
||||
flags: []string{"--login-server=https://controlplane.tailscale.com"},
|
||||
@@ -427,6 +446,38 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
||||
},
|
||||
{
|
||||
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
||||
// migth've had old an install, and we don't support --accept-routes anyway.
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: distro.Synology,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Same test case as "synology_permit_omit_accept_routes" above, but
|
||||
// on non-Synology distro.
|
||||
name: "not_synology_dont_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: "", // not Synology
|
||||
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -447,6 +498,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
@@ -495,6 +547,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
@@ -532,7 +585,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
@@ -733,6 +786,33 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "just_edit_reset",
|
||||
flags: []string{"--reset"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
AdvertiseRoutesSet: true,
|
||||
AdvertiseTagsSet: true,
|
||||
AllowSingleHostsSet: true,
|
||||
ControlURLSet: true,
|
||||
CorpDNSSet: true,
|
||||
ExitNodeAllowLANAccessSet: true,
|
||||
ExitNodeIDSet: true,
|
||||
ExitNodeIPSet: true,
|
||||
HostnameSet: true,
|
||||
NetfilterModeSet: true,
|
||||
NoSNATSet: true,
|
||||
OperatorUserSet: true,
|
||||
RouteAllSet: true,
|
||||
RunSSHSet: true,
|
||||
ShieldsUpSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "control_synonym",
|
||||
flags: []string{},
|
||||
@@ -797,11 +877,148 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
if simpleUp != tt.wantSimpleUp {
|
||||
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
||||
}
|
||||
var oldEditPrefs ipn.Prefs
|
||||
if justEditMP != nil {
|
||||
oldEditPrefs = justEditMP.Prefs
|
||||
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
||||
}
|
||||
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
||||
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
|
||||
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var cmpIP = cmp.Comparer(func(a, b netaddr.IP) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
func TestExitNodeIPOfArg(t *testing.T) {
|
||||
mustIP := netaddr.MustParseIP
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
st *ipnstate.Status
|
||||
want netaddr.IP
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "ip_while_stopped_okay",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Stopped",
|
||||
},
|
||||
want: mustIP("1.2.3.4"),
|
||||
},
|
||||
{
|
||||
name: "ip_not_found",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
},
|
||||
wantErr: `no node found in netmap with IP 1.2.3.4`,
|
||||
},
|
||||
{
|
||||
name: "ip_not_exit",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `node 1.2.3.4 is not advertising an exit node`,
|
||||
},
|
||||
{
|
||||
name: "ip",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: mustIP("1.2.3.4"),
|
||||
},
|
||||
{
|
||||
name: "no_match",
|
||||
arg: "unknown",
|
||||
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
|
||||
wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: mustIP("1.0.0.2"),
|
||||
},
|
||||
{
|
||||
name: "name_not_exit",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `node "skippy" is not advertising an exit node`,
|
||||
},
|
||||
{
|
||||
name: "ambiguous",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "SKIPPY.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `ambiguous exit node name "skippy"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := exitNodeIPOfArg(tt.arg, tt.st)
|
||||
if err != nil {
|
||||
if err.Error() == tt.wantErr {
|
||||
return
|
||||
}
|
||||
if tt.wantErr == "" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Fatalf("error = %#q; want %#q", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
t.Fatalf("got %v; want error %#q", got, tt.wantErr)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
85
cmd/tailscale/cli/configure-host.go
Normal file
85
cmd/tailscale/cli/configure-host.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var configureHostCmd = &ffcli.Command{
|
||||
Name: "configure-host",
|
||||
Exec: runConfigureHost,
|
||||
ShortHelp: "Configure Synology to enable more Tailscale features",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure-host' command is intended to run at boot as root
|
||||
to create the /dev/net/tun device and give the tailscaled binary
|
||||
permission to use it.
|
||||
|
||||
See: https://tailscale.com/kb/1152/synology-outbound/
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure-host")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var configureHostArgs struct{}
|
||||
|
||||
func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return errors.New("only implemented on Synology")
|
||||
}
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
osVer := hostinfo.GetOSVersion()
|
||||
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
|
||||
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
return fmt.Errorf("unsupported DSM version %q", osVer)
|
||||
}
|
||||
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("/dev/net", 0755); err != nil {
|
||||
return fmt.Errorf("creating /dev/net: %v", err)
|
||||
}
|
||||
if out, err := exec.Command("/bin/mknod", "/dev/net/tun", "c", "10", "200").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("creating /dev/net/tun: %v, %s", err, out)
|
||||
}
|
||||
}
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
|
||||
const daemonBin = "/var/packages/Tailscale/target/bin/tailscaled"
|
||||
if _, err := os.Stat(daemonBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("tailscaled binary not found at %s. Is the Tailscale *.spk package installed?", daemonBin)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if out, err := exec.Command("/bin/setcap", "cap_net_admin+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")
|
||||
return nil
|
||||
}
|
||||
@@ -70,6 +70,16 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runLocalCreds,
|
||||
ShortHelp: "print how to access Tailscale local API",
|
||||
},
|
||||
{
|
||||
Name: "restun",
|
||||
Exec: localAPIAction("restun"),
|
||||
ShortHelp: "force a magicsock restun",
|
||||
},
|
||||
{
|
||||
Name: "rebind",
|
||||
Exec: localAPIAction("rebind"),
|
||||
ShortHelp: "force a magicsock rebind",
|
||||
},
|
||||
{
|
||||
Name: "prefs",
|
||||
Exec: runPrefs,
|
||||
@@ -244,6 +254,15 @@ func runDERPMap(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func localAPIAction(action string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected arguments")
|
||||
}
|
||||
return tailscale.DebugAction(ctx, action)
|
||||
}
|
||||
}
|
||||
|
||||
func runEnv(ctx context.Context, args []string) error {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
|
||||
@@ -25,23 +25,23 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
|
||||
}
|
||||
found := false
|
||||
var foundProc ps.Process
|
||||
for _, proc := range procs {
|
||||
base := filepath.Base(proc.Executable())
|
||||
if base == "tailscaled" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "darwin" && base == "IPNExtension" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if foundProc == nil {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
|
||||
@@ -52,5 +52,5 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
|
||||
}
|
||||
|
||||
@@ -11,14 +11,11 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -28,8 +25,10 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
|
||||
stableID, isOffline, err := getTargetStableID(ctx, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
@@ -149,37 +148,26 @@ func runCp(ctx context.Context, args []string) error {
|
||||
name = filepath.Base(fileArg)
|
||||
}
|
||||
|
||||
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
|
||||
if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
|
||||
fileContents = &slowReader{r: fileContents}
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
}
|
||||
io.Copy(Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
@@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, isOffline, nil
|
||||
return n.StableID, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
@@ -336,7 +324,7 @@ func runFileGet(ctx context.Context, args []string) error {
|
||||
for {
|
||||
wfs, err = tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
return fmt.Errorf("getting WaitingFiles: %w", err)
|
||||
}
|
||||
if len(wfs) != 0 || !getArgs.wait {
|
||||
break
|
||||
@@ -391,7 +379,7 @@ func wipeInbox(ctx context.Context) error {
|
||||
}
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
return fmt.Errorf("getting WaitingFiles: %w", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
|
||||
@@ -18,12 +18,13 @@ import (
|
||||
|
||||
var ipCmd = &ffcli.Command{
|
||||
Name: "ip",
|
||||
ShortUsage: "ip [-4] [-6] [peername]",
|
||||
ShortHelp: "Show current Tailscale IP address(es)",
|
||||
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
|
||||
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
|
||||
ShortHelp: "Show Tailscale IP addresses",
|
||||
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
|
||||
Exec: runIP,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ip")
|
||||
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
|
||||
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
|
||||
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
|
||||
return fs
|
||||
@@ -31,13 +32,14 @@ var ipCmd = &ffcli.Command{
|
||||
}
|
||||
|
||||
var ipArgs struct {
|
||||
want1 bool
|
||||
want4 bool
|
||||
want6 bool
|
||||
}
|
||||
|
||||
func runIP(ctx context.Context, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return errors.New("unknown arguments")
|
||||
return errors.New("too many arguments, expected at most one peer")
|
||||
}
|
||||
var of string
|
||||
if len(args) == 1 {
|
||||
@@ -45,8 +47,14 @@ func runIP(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
v4, v6 := ipArgs.want4, ipArgs.want6
|
||||
if v4 && v6 {
|
||||
return errors.New("tailscale ip -4 and -6 are mutually exclusive")
|
||||
nflags := 0
|
||||
for _, b := range []bool{ipArgs.want1, v4, v6} {
|
||||
if b {
|
||||
nflags++
|
||||
}
|
||||
}
|
||||
if nflags > 1 {
|
||||
return errors.New("tailscale ip -1, -4, and -6 are mutually exclusive")
|
||||
}
|
||||
if !v4 && !v6 {
|
||||
v4, v6 = true, true
|
||||
@@ -71,6 +79,9 @@ func runIP(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
|
||||
}
|
||||
|
||||
if ipArgs.want1 {
|
||||
ips = ips[:1]
|
||||
}
|
||||
match := false
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
@@ -49,7 +49,7 @@ var netcheckArgs struct {
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -64,6 +65,16 @@ var pingArgs struct {
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
printf("%s\n", description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -29,7 +29,22 @@ var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
JSON FORMAT
|
||||
|
||||
Warning: this format has changed between releases and might change more
|
||||
in the future.
|
||||
|
||||
For a description of the fields, see the "type Status" declaration at:
|
||||
|
||||
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
|
||||
|
||||
(and be sure to select branch/tag that corresponds to the version
|
||||
of Tailscale you're running)
|
||||
|
||||
`),
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
@@ -106,24 +121,10 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
outln(description)
|
||||
os.Exit(1)
|
||||
case ipn.Stopped.String():
|
||||
outln("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
outln("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
outln("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
if len(st.Health) > 0 {
|
||||
@@ -145,11 +146,19 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
var offline string
|
||||
if !ps.Online {
|
||||
offline = "; offline"
|
||||
}
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
f("idle; exit node" + offline)
|
||||
} else if ps.ExitNodeOption {
|
||||
f("idle; offers exit node" + offline)
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
f("idle" + offline)
|
||||
} else if !ps.Online {
|
||||
f("offline")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
@@ -157,12 +166,17 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
} else if ps.ExitNodeOption {
|
||||
f("offers exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
if !ps.Online {
|
||||
f("; offline")
|
||||
}
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
@@ -194,6 +208,27 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
// It also returns a description of the status suitable to display to a user.
|
||||
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
switch st.BackendState {
|
||||
default:
|
||||
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
|
||||
case ipn.Stopped.String():
|
||||
return "Tailscale is stopped.", false
|
||||
case ipn.NeedsLogin.String():
|
||||
s := "Logged out."
|
||||
if st.AuthURL != "" {
|
||||
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
|
||||
}
|
||||
return s, false
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
return "Machine is not yet authorized by tailnet admin.", false
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
return st.BackendState, true
|
||||
}
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
|
||||
@@ -6,6 +6,8 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -22,12 +24,15 @@ import (
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -46,8 +51,10 @@ down").
|
||||
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset
|
||||
flag is also used.
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --authkey, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
@@ -60,22 +67,41 @@ func effectiveGOOS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
||||
// a function of the platform it's running on.
|
||||
func acceptRouteDefault(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := newFlagSet("up")
|
||||
|
||||
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
||||
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
if envknob.UseWIPCode() || inTest() {
|
||||
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
}
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKeyOrFile, "authkey", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
@@ -111,6 +137,7 @@ type upArgsT struct {
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
forceReauth bool
|
||||
forceDaemon bool
|
||||
advertiseRoutes string
|
||||
@@ -121,6 +148,7 @@ type upArgsT struct {
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
hostname string
|
||||
opUser string
|
||||
json bool
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -138,6 +166,33 @@ func (a upArgsT) getAuthKey() (string, error) {
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
|
||||
//
|
||||
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
|
||||
// providing the link for where to authenticate this client. BackendState would be
|
||||
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
|
||||
// populated if something goes badly wrong.
|
||||
//
|
||||
// When the client is authenticated by having someone visit the AuthURL, a second
|
||||
// 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"
|
||||
// }
|
||||
//
|
||||
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
|
||||
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
|
||||
Error string `json:",omitempty"` // description of an error
|
||||
}
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
@@ -190,6 +245,65 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// peerWithTailscaleIP returns the peer in st with the provided
|
||||
// Tailscale IP.
|
||||
func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) {
|
||||
for _, ps := range st.Peer {
|
||||
for _, ip2 := range ps.TailscaleIPs {
|
||||
if ip == ip2 {
|
||||
return ps, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// exitNodeIPOfArg maps from a user-provided CLI flag value to an IP
|
||||
// address they want to use as an exit node.
|
||||
func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) {
|
||||
if arg == "" {
|
||||
return ip, errors.New("invalid use of exitNodeIPOfArg with empty string")
|
||||
}
|
||||
ip, err = netaddr.ParseIP(arg)
|
||||
if err == nil {
|
||||
// If we're online already and have a netmap, double check that the IP
|
||||
// address specified is valid.
|
||||
if st.BackendState == "Running" {
|
||||
ps, ok := peerWithTailscaleIP(st, ip)
|
||||
if !ok {
|
||||
return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
|
||||
}
|
||||
if !ps.ExitNodeOption {
|
||||
return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
|
||||
}
|
||||
}
|
||||
return ip, err
|
||||
}
|
||||
match := 0
|
||||
for _, ps := range st.Peer {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if !strings.EqualFold(arg, baseName) {
|
||||
continue
|
||||
}
|
||||
match++
|
||||
if len(ps.TailscaleIPs) == 0 {
|
||||
return ip, fmt.Errorf("node %q has no Tailscale IP?", arg)
|
||||
}
|
||||
if !ps.ExitNodeOption {
|
||||
return ip, fmt.Errorf("node %q is not advertising an exit node", arg)
|
||||
}
|
||||
ip = ps.TailscaleIPs[0]
|
||||
}
|
||||
switch match {
|
||||
case 0:
|
||||
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", arg)
|
||||
case 1:
|
||||
return ip, nil
|
||||
default:
|
||||
return ip, fmt.Errorf("ambiguous exit node name %q", arg)
|
||||
}
|
||||
}
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
@@ -205,9 +319,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
return nil, err
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
@@ -245,6 +359,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.RunSSH = upArgs.runSSH
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.Hostname = upArgs.hostname
|
||||
@@ -272,7 +387,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
// updatePrefs updates prefs based on curPrefs
|
||||
// updatePrefs returns how to edit preferences based on the
|
||||
// flag-provided 'prefs' and the currently active 'curPrefs'.
|
||||
//
|
||||
// It returns a non-nil justEditMP if we're already running and none of
|
||||
// the flags require a restart, so we can just do an EditPrefs call and
|
||||
@@ -306,15 +422,19 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
|
||||
justEdit := env.backendState == ipn.Running.String() &&
|
||||
!env.upArgs.forceReauth &&
|
||||
!env.upArgs.reset &&
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
justEditMP.Prefs = *prefs
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
visitFlags := env.flagSet.Visit
|
||||
if env.upArgs.reset {
|
||||
visitFlags = env.flagSet.VisitAll
|
||||
}
|
||||
visitFlags(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
||||
})
|
||||
}
|
||||
@@ -380,11 +500,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
distro: distro.Get(),
|
||||
user: os.Getenv("USER"),
|
||||
flagSet: upFlagSet,
|
||||
upArgs: upArgs,
|
||||
backendState: st.BackendState,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
@@ -405,7 +526,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
printed := !simpleUp
|
||||
var printed bool // whether we've yet printed anything to stdout or stderr
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
@@ -431,14 +552,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
@@ -451,15 +577,33 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
log.Printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -546,6 +690,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
||||
data, err := json.MarshalIndent(js, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("printUpDoneJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
)
|
||||
@@ -571,6 +725,7 @@ func init() {
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
addPrefFlagMapping("ssh", "RunSSH")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@@ -588,7 +743,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset", "qr":
|
||||
case "authkey", "force-reauth", "reset", "qr", "json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -622,6 +777,7 @@ type upCheckEnv struct {
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
distro distro.Distro
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
@@ -672,6 +828,10 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
||||
continue
|
||||
}
|
||||
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
|
||||
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
@@ -756,6 +916,8 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interfac
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "ssh":
|
||||
set(prefs.RunSSH)
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
|
||||
@@ -270,14 +270,14 @@ func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
// We need a SynoToken for authenticate.cgi.
|
||||
// So we tell the client to get one.
|
||||
serverURL := r.URL.Scheme + "://" + r.URL.Host
|
||||
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
|
||||
synoTokenRedirectHTML.Execute(w, serverURL)
|
||||
return true
|
||||
}
|
||||
|
||||
const synoTokenRedirectHTML = `<html><body>
|
||||
var synoTokenRedirectHTML = template.Must(template.New("redirect").Parse(`<html><body>
|
||||
Redirecting with session token...
|
||||
<script>
|
||||
var serverURL = %q;
|
||||
var serverURL = {{ . }};
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("GET", serverURL + "/webman/login.cgi", true);
|
||||
@@ -289,7 +289,7 @@ req.onload = function() {
|
||||
req.send(null);
|
||||
</script>
|
||||
</body></html>
|
||||
`
|
||||
`))
|
||||
|
||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if authRedirect(w, r) {
|
||||
@@ -375,7 +375,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.AdvertiseExitNode = true
|
||||
} else {
|
||||
if data.AdvertiseRoutes != "" {
|
||||
data.AdvertiseRoutes = ","
|
||||
data.AdvertiseRoutes += ","
|
||||
}
|
||||
data.AdvertiseRoutes += r.String()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -37,7 +38,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
@@ -46,6 +48,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/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/packet from tailscale.com/wgengine/filter
|
||||
@@ -72,7 +75,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/util/clientmetric from tailscale.com/net/netcheck
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
@@ -91,7 +94,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
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
|
||||
@@ -103,7 +106,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
@@ -224,7 +225,7 @@ func debugPortmap(ctx context.Context) error {
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
|
||||
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
|
||||
@@ -3,6 +3,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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
|
||||
L github.com/anmitsu/go-shlex from github.com/gliderlabs/ssh
|
||||
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/aws
|
||||
@@ -25,7 +26,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
@@ -59,10 +61,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
L 💣 github.com/creack/pty from tailscale.com/wgengine/netstack
|
||||
L github.com/gliderlabs/ssh from tailscale.com/wgengine/netstack
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -113,39 +118,46 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2
|
||||
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
inet.af/netaddr from inet.af/wf+
|
||||
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
|
||||
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/atomicbitops+
|
||||
inet.af/netstack/state/wire from inet.af/netstack/state
|
||||
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
|
||||
inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
|
||||
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
|
||||
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
|
||||
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/tcpconntrack from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
@@ -162,6 +174,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
@@ -172,13 +185,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
💣 tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/net/dns+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
@@ -186,16 +199,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netknob from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 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/portmapper from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
|
||||
tailscale.com/net/socks5/tssocks from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
@@ -208,6 +224,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
@@ -217,7 +234,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
@@ -243,7 +260,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
@@ -252,21 +269,26 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
L golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
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+
|
||||
L golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
|
||||
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/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
L golang.org/x/crypto/ssh from github.com/gliderlabs/ssh+
|
||||
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/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 net/http+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
@@ -285,26 +307,26 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
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/rc4 from crypto/tls+
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
@@ -328,9 +350,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from inet.af/netstack/tcpip/network/ipv6+
|
||||
hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
html/template from tailscale.com/tsweb
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
@@ -357,7 +380,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
@@ -369,6 +392,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
|
||||
@@ -23,21 +23,25 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -175,8 +179,7 @@ func main() {
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +224,7 @@ func statePathOrDefault() string {
|
||||
func ipnServerOpts() (o ipnserver.Options) {
|
||||
// Allow changing the OS-specific IPN behavior for tests
|
||||
// so we can e.g. test Windows-specific behaviors on Linux.
|
||||
goos := os.Getenv("TS_DEBUG_TAILSCALED_IPN_GOOS")
|
||||
goos := envknob.String("TS_DEBUG_TAILSCALED_IPN_GOOS")
|
||||
if goos == "" {
|
||||
goos = runtime.GOOS
|
||||
}
|
||||
@@ -269,13 +272,13 @@ func run() error {
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
if envknob.Bool("TS_DEBUG_MEMORY") {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
if os.Getenv("TS_PLEASE_PANIC") != "" {
|
||||
if envknob.Bool("TS_PLEASE_PANIC") {
|
||||
panic("TS_PLEASE_PANIC asked us to panic")
|
||||
}
|
||||
dns.Cleanup(logf, args.tunname)
|
||||
@@ -293,45 +296,63 @@ func run() error {
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
|
||||
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
}
|
||||
if debugMux != nil {
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if _, mc, ok := ig.GetInternals(); ok {
|
||||
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
|
||||
}
|
||||
}
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, e)
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
}
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
go func() {
|
||||
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
||||
}()
|
||||
}
|
||||
if socksListener != nil {
|
||||
ss := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
Dialer: dialer.UserDial,
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -361,12 +382,15 @@ func run() error {
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.StateStore: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
ns.SetLocalBackend(srv.LocalBackend())
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
@@ -381,21 +405,20 @@ func run() error {
|
||||
err = srv.Run(ctx, ln)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
if err == nil {
|
||||
return e, useNetstack, nil
|
||||
}
|
||||
@@ -408,11 +431,7 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, us
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
|
||||
v, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
|
||||
}
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
|
||||
return v
|
||||
}
|
||||
if distro.Get() == distro.Synology {
|
||||
@@ -427,10 +446,11 @@ func shouldWrapNetstack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
}
|
||||
|
||||
useNetstack = name == "userspace-networking"
|
||||
@@ -440,14 +460,14 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
|
||||
}
|
||||
}
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
@@ -459,11 +479,11 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("creating router: %w", err)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
@@ -491,6 +511,7 @@ func newDebugMux() *http.ServeMux {
|
||||
|
||||
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
tsweb.VarzHandler(w, r)
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
@@ -504,26 +525,54 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
return netstack.Create(logf, tunDev, e, magicConn)
|
||||
return netstack.Create(logf, tunDev, e, magicConn, dialer)
|
||||
}
|
||||
|
||||
func mustStartTCPListener(name, addr string) net.Listener {
|
||||
if addr == "" {
|
||||
return nil
|
||||
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
|
||||
// proxies, if the respective addresses are not empty. socksAddr and
|
||||
// httpAddr can be the same, in which case socksListener will receive
|
||||
// connections that look like they're speaking SOCKS and httpListener
|
||||
// will receive everything else.
|
||||
//
|
||||
// socksListener and httpListener can be nil, if their respective
|
||||
// addrs are empty.
|
||||
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
|
||||
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
|
||||
ln, err := net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("proxy listener: %v", err)
|
||||
}
|
||||
return proxymux.SplitSOCKSAndHTTP(ln)
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("%v listener: %v", name, err)
|
||||
|
||||
var err error
|
||||
if socksAddr != "" {
|
||||
socksListener, err = net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(addr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("%v listening on %v", name, ln.Addr())
|
||||
if httpAddr != "" {
|
||||
httpListener, err = net.Listen("tcp", httpAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("HTTP proxy listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(httpAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
|
||||
}
|
||||
}
|
||||
return ln
|
||||
|
||||
return socksListener, httpListener
|
||||
}
|
||||
|
||||
@@ -29,9 +29,11 @@ import (
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -39,6 +41,7 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -53,6 +56,11 @@ func isWindowsService() bool {
|
||||
return v
|
||||
}
|
||||
|
||||
// runWindowsService starts running Tailscale under the Windows
|
||||
// Service environment.
|
||||
//
|
||||
// At this point we're still the parent process that
|
||||
// Windows started.
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
@@ -66,7 +74,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
svcAccepts := svc.AcceptStop
|
||||
if winutil.GetRegInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
svcAccepts |= svc.AcceptSessionChange
|
||||
}
|
||||
|
||||
@@ -78,7 +86,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
// Make a logger without a date prefix, as filelogger
|
||||
// and logtail both already add their own. All we really want
|
||||
// from the log package is the automatic newline.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
// We start with log.Default().Writer(), which is the logtail
|
||||
// writer that logpolicy already installed as the global
|
||||
// output.
|
||||
logger := log.New(log.Default().Writer(), "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
@@ -88,6 +99,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
select {
|
||||
case <-doneCh:
|
||||
case cmd := <-r:
|
||||
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
@@ -104,6 +116,42 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func cmdName(c svc.Cmd) string {
|
||||
switch c {
|
||||
case svc.Stop:
|
||||
return "Stop"
|
||||
case svc.Pause:
|
||||
return "Pause"
|
||||
case svc.Continue:
|
||||
return "Continue"
|
||||
case svc.Interrogate:
|
||||
return "Interrogate"
|
||||
case svc.Shutdown:
|
||||
return "Shutdown"
|
||||
case svc.ParamChange:
|
||||
return "ParamChange"
|
||||
case svc.NetBindAdd:
|
||||
return "NetBindAdd"
|
||||
case svc.NetBindRemove:
|
||||
return "NetBindRemove"
|
||||
case svc.NetBindEnable:
|
||||
return "NetBindEnable"
|
||||
case svc.NetBindDisable:
|
||||
return "NetBindDisable"
|
||||
case svc.DeviceEvent:
|
||||
return "DeviceEvent"
|
||||
case svc.HardwareProfileChange:
|
||||
return "HardwareProfileChange"
|
||||
case svc.PowerEvent:
|
||||
return "PowerEvent"
|
||||
case svc.SessionChange:
|
||||
return "SessionChange"
|
||||
case svc.PreShutdown:
|
||||
return "PreShutdown"
|
||||
}
|
||||
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if beFirewallKillswitch() {
|
||||
return true
|
||||
@@ -114,6 +162,9 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file logggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
@@ -177,6 +228,12 @@ func beFirewallKillswitch() bool {
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer := new(tsdial.Dialer)
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
@@ -197,17 +254,19 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
ns, err := newNetstack(logf, eng)
|
||||
ns, err := newNetstack(logf, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
@@ -256,7 +315,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
}
|
||||
for {
|
||||
@@ -287,7 +346,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
|
||||
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
|
||||
@@ -157,8 +157,9 @@ func handleSSH(s ssh.Session) {
|
||||
cmd.Process.Kill()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
s.Exit(1)
|
||||
} else {
|
||||
s.Exit(0)
|
||||
}
|
||||
s.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package noise implements the base transport of the Tailscale 2021
|
||||
// control protocol.
|
||||
// Package controlbase implements the base transport of the Tailscale
|
||||
// 2021 control protocol.
|
||||
//
|
||||
// The base transport implements Noise IK, instantiated with
|
||||
// Curve25519, ChaCha20Poly1305 and BLAKE2s.
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -202,7 +202,7 @@ func TestConnStd(t *testing.T) {
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
c2, err = Server(context.Background(), s2, controlKey)
|
||||
c2, err = Server(context.Background(), s2, controlKey, nil)
|
||||
serverErr <- err
|
||||
}()
|
||||
c1, err = Client(context.Background(), s1, machineKey, controlKey.Public())
|
||||
@@ -319,7 +319,7 @@ func pairWithConns(t *testing.T, clientConn, serverConn net.Conn) (*Conn, *Conn)
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, controlKey)
|
||||
server, err = Server(context.Background(), serverConn, controlKey, nil)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -50,21 +50,23 @@ func protocolVersionPrologue(version uint16) []byte {
|
||||
return strconv.AppendUint(ret, uint64(version), 10)
|
||||
}
|
||||
|
||||
// Client initiates a control client handshake, returning the resulting
|
||||
// control connection.
|
||||
//
|
||||
// The context deadline, if any, covers the entire handshaking
|
||||
// process. Any preexisting Conn deadline is removed.
|
||||
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
conn.SetDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
// HandshakeContinuation upgrades a net.Conn to a Conn. The net.Conn
|
||||
// is assumed to have already sent the client>server handshake
|
||||
// initiation message.
|
||||
type HandshakeContinuation func(context.Context, net.Conn) (*Conn, error)
|
||||
|
||||
// ClientDeferred initiates a control client handshake, returning the
|
||||
// initial message to send to the server and a continuation to
|
||||
// finalize the handshake.
|
||||
//
|
||||
// ClientDeferred is split in this way for RTT reduction: we run this
|
||||
// protocol after negotiating a protocol switch from HTTP/HTTPS. If we
|
||||
// completely serialized the negotiation followed by the handshake,
|
||||
// we'd pay an extra RTT to transmit the handshake initiation after
|
||||
// protocol switching. By splitting the handshake into an initial
|
||||
// message and a continuation, we can embed the handshake initiation
|
||||
// into the HTTP protocol switching request and avoid a bit of delay.
|
||||
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
|
||||
var s symmetricState
|
||||
s.Initialize()
|
||||
|
||||
@@ -83,18 +85,53 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, c
|
||||
s.MixHash(machineEphemeralPub.UntypedBytes())
|
||||
cipher, err := s.MixDH(machineEphemeral, controlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing es: %w", err)
|
||||
return nil, nil, fmt.Errorf("computing es: %w", err)
|
||||
}
|
||||
machineKeyPub := machineKey.Public()
|
||||
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub.UntypedBytes())
|
||||
cipher, err = s.MixDH(machineKey, controlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing ss: %w", err)
|
||||
return nil, nil, fmt.Errorf("computing ss: %w", err)
|
||||
}
|
||||
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
|
||||
|
||||
if _, err := conn.Write(init[:]); err != nil {
|
||||
return nil, fmt.Errorf("writing initiation: %w", err)
|
||||
cont := func(ctx context.Context, conn net.Conn) (*Conn, error) {
|
||||
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey)
|
||||
}
|
||||
return init[:], cont, nil
|
||||
}
|
||||
|
||||
// Client wraps ClientDeferred and immediately invokes the returned
|
||||
// continuation with conn.
|
||||
//
|
||||
// This is a helper for when you don't need the fancy
|
||||
// continuation-style handshake, and just want to synchronously
|
||||
// upgrade a net.Conn to a secure transport.
|
||||
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
|
||||
init, cont, err := ClientDeferred(machineKey, controlKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(init); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cont(ctx, conn)
|
||||
}
|
||||
|
||||
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
|
||||
// No matter what, this function can only run once per s. Ensure
|
||||
// attempted reuse causes a panic.
|
||||
defer func() {
|
||||
s.finished = true
|
||||
}()
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
conn.SetDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
|
||||
// Read in the payload and look for errors/protocol violations from the server.
|
||||
@@ -122,10 +159,10 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, c
|
||||
// <- e, ee, se
|
||||
controlEphemeralPub := key.MachinePublicFromRaw32(mem.B(resp.EphemeralPub()))
|
||||
s.MixHash(controlEphemeralPub.UntypedBytes())
|
||||
if _, err = s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
|
||||
if _, err := s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
|
||||
return nil, fmt.Errorf("computing ee: %w", err)
|
||||
}
|
||||
cipher, err = s.MixDH(machineKey, controlEphemeralPub)
|
||||
cipher, err := s.MixDH(machineKey, controlEphemeralPub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing se: %w", err)
|
||||
}
|
||||
@@ -156,9 +193,13 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, c
|
||||
// Server initiates a control server handshake, returning the resulting
|
||||
// control connection.
|
||||
//
|
||||
// optionalInit can be the client's initial handshake message as
|
||||
// returned by ClientDeferred, or nil in which case the initial
|
||||
// message is read from conn.
|
||||
//
|
||||
// The context deadline, if any, covers the entire handshaking
|
||||
// process.
|
||||
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (*Conn, error) {
|
||||
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, optionalInit []byte) (*Conn, error) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
@@ -190,7 +231,12 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (
|
||||
s.Initialize()
|
||||
|
||||
var init initiationMessage
|
||||
if _, err := io.ReadFull(conn, init.Header()); err != nil {
|
||||
if optionalInit != nil {
|
||||
if len(optionalInit) != len(init) {
|
||||
return nil, sendErr("wrong handshake initiation size")
|
||||
}
|
||||
copy(init[:], optionalInit)
|
||||
} else if _, err := io.ReadFull(conn, init.Header()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if init.Version() != protocolVersion {
|
||||
@@ -202,8 +248,11 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate) (
|
||||
if init.Length() != len(init.Payload()) {
|
||||
return nil, sendErr("wrong handshake initiation length")
|
||||
}
|
||||
if _, err := io.ReadFull(conn, init.Payload()); err != nil {
|
||||
return nil, err
|
||||
// if optionalInit was provided, we have the payload already.
|
||||
if optionalInit == nil {
|
||||
if _, err := io.ReadFull(conn, init.Payload()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// prologue. Can only do this once we at least think the client is
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -26,7 +26,7 @@ func TestHandshake(t *testing.T) {
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, serverKey)
|
||||
server, err = Server(context.Background(), serverConn, serverKey, nil)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestNoReuse(t *testing.T) {
|
||||
)
|
||||
go func() {
|
||||
var err error
|
||||
server, err = Server(context.Background(), serverConn, serverKey)
|
||||
server, err = Server(context.Background(), serverConn, serverKey, nil)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
@@ -139,6 +139,9 @@ func TestNoReuse(t *testing.T) {
|
||||
t.Fatalf("server wire traffic seen twice")
|
||||
}
|
||||
packets[serverWire] = true
|
||||
|
||||
server.Close()
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +172,7 @@ func TestTampering(t *testing.T) {
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverConn, serverKey)
|
||||
_, err := Server(context.Background(), serverConn, serverKey, nil)
|
||||
// If the server failed, we have to close the Conn to
|
||||
// unblock the client.
|
||||
if err != nil {
|
||||
@@ -197,7 +200,7 @@ func TestTampering(t *testing.T) {
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverConn, serverKey)
|
||||
_, err := Server(context.Background(), serverConn, serverKey, nil)
|
||||
serverErr <- err
|
||||
}()
|
||||
|
||||
@@ -222,7 +225,7 @@ func TestTampering(t *testing.T) {
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
server, err := Server(context.Background(), serverConn, serverKey)
|
||||
server, err := Server(context.Background(), serverConn, serverKey, nil)
|
||||
serverErr <- err
|
||||
_, err = io.WriteString(server, strings.Repeat("a", 14))
|
||||
serverErr <- err
|
||||
@@ -263,7 +266,7 @@ func TestTampering(t *testing.T) {
|
||||
serverErr = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
server, err := Server(context.Background(), serverConn, serverKey)
|
||||
server, err := Server(context.Background(), serverConn, serverKey, nil)
|
||||
serverErr <- err
|
||||
var bs [100]byte
|
||||
// The server needs a timeout if the tampering is hitting the length header.
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -29,7 +29,7 @@ func TestInteropClient(t *testing.T) {
|
||||
)
|
||||
|
||||
go func() {
|
||||
server, err := Server(context.Background(), s2, controlKey)
|
||||
server, err := Server(context.Background(), s2, controlKey, nil)
|
||||
serverErr <- err
|
||||
if err != nil {
|
||||
return
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
@@ -24,7 +24,7 @@ IK:
|
||||
* PARAMETERS *
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
package noise
|
||||
package controlbase
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -266,9 +266,9 @@ func (c *Auto) authRoutine() {
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
c.logf("[v1] authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
c.logf("[v1] authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -339,11 +339,9 @@ func (c *Auto) authRoutine() {
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
if goal.url != "" {
|
||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
|
||||
// goal.url ought to be empty here.
|
||||
// However, not all control servers get this right,
|
||||
// and logging about it here just generates noise.
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: true,
|
||||
@@ -416,7 +414,7 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.logf("mapRoutine: %s", c.state)
|
||||
c.logf("[v1] mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
ctx := c.mapCtx
|
||||
c.mu.Unlock()
|
||||
@@ -447,9 +445,9 @@ func (c *Auto) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("mapRoutine: context done.")
|
||||
c.logf("[v1] mapRoutine: context done.")
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map needed while idle.")
|
||||
c.logf("[v1] mapRoutine: new map needed while idle.")
|
||||
}
|
||||
} else {
|
||||
// Be sure this is false when we're not inside
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -30,6 +29,7 @@ import (
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -160,7 +160,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
dialer := netns.NewDialer(opts.Logf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
@@ -168,6 +168,10 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
// Disable implicit gzip compression; the various
|
||||
// handlers (register, map, set-dns, etc) do their own
|
||||
// zstd compression per naclbox.
|
||||
tr.DisableCompression = true
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
}
|
||||
c.hostinfo = hi.Clone()
|
||||
j, _ := json.Marshal(c.hostinfo)
|
||||
c.logf("HostInfo: %s", j)
|
||||
c.logf("[v1] HostInfo: %s", j)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -241,10 +245,10 @@ func (c *Direct) GetPersist() persist.Persist {
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
c.logf("direct.TryLogout()")
|
||||
c.logf("[v1] direct.TryLogout()")
|
||||
|
||||
mustRegen, newURL, err := c.doLogin(ctx, loginOpt{Logout: true})
|
||||
c.logf("TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
|
||||
c.logf("[v1] TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
|
||||
|
||||
c.mu.Lock()
|
||||
c.persist = persist.Persist{}
|
||||
@@ -254,7 +258,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
c.logf("[v1] direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, loginOpt{Token: t, Flags: flags})
|
||||
}
|
||||
|
||||
@@ -262,7 +266,7 @@ func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Log
|
||||
//
|
||||
// On success, newURL and err will both be nil.
|
||||
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newURL string, err error) {
|
||||
c.logf("direct.WaitLoginURL")
|
||||
c.logf("[v1] direct.WaitLoginURL")
|
||||
return c.doLoginOrRegen(ctx, loginOpt{URL: url})
|
||||
}
|
||||
|
||||
@@ -465,7 +469,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if resp.AuthURL != "" {
|
||||
c.logf("AuthURL is %v", resp.AuthURL)
|
||||
} else {
|
||||
c.logf("No AuthURL")
|
||||
c.logf("[v1] No AuthURL")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -516,7 +520,7 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (c
|
||||
for _, ep := range endpoints {
|
||||
epStrs = append(epStrs, ep.Addr.String())
|
||||
}
|
||||
c.logf("client.newEndpoints(%v, %v)", localPort, epStrs)
|
||||
c.logf("[v2] client.newEndpoints(%v, %v)", localPort, epStrs)
|
||||
c.localPort = localPort
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
@@ -821,10 +825,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
p.Endpoints = nil
|
||||
}
|
||||
}
|
||||
if Debug.StripCaps {
|
||||
@@ -874,8 +875,8 @@ func decode(res *http.Response, v interface{}, serverKey key.MachinePublic, mkey
|
||||
}
|
||||
|
||||
var (
|
||||
debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||
debugRegister, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_REGISTER"))
|
||||
debugMap = envknob.Bool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
@@ -985,26 +986,14 @@ type debug struct {
|
||||
|
||||
func initDebug() debug {
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
|
||||
Disco: os.Getenv("TS_DEBUG_USE_DISCO") == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
NetMap: envknob.Bool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
|
||||
Disco: envknob.BoolDefaultTrue("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
func envBool(k string) bool {
|
||||
e := os.Getenv(k)
|
||||
if e == "" {
|
||||
return false
|
||||
}
|
||||
v, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid non-bool %q for env var %q", e, k))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var clockNow = time.Now
|
||||
|
||||
// opt.Bool configs from control.
|
||||
|
||||
@@ -6,11 +6,10 @@ package controlclient
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -289,7 +288,7 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
return v2
|
||||
}
|
||||
|
||||
var debugSelfIPv6Only, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_SELF_V6_ONLY"))
|
||||
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
|
||||
|
||||
func filterSelfAddresses(in []netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
|
||||
switch {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -73,23 +74,46 @@ func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func selectIdentityFromSlice(subject string, ids []certstore.Identity) (certstore.Identity, []*x509.Certificate) {
|
||||
func selectIdentityFromSlice(subject string, ids []certstore.Identity, now time.Time) (certstore.Identity, []*x509.Certificate) {
|
||||
var bestCandidate struct {
|
||||
id certstore.Identity
|
||||
chain []*x509.Certificate
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
chain, err := id.CertificateChain()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(chain) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isSupportedCertificate(chain[0]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isSubjectInChain(subject, chain) {
|
||||
return id, chain
|
||||
if now.Before(chain[0].NotBefore) || now.After(chain[0].NotAfter) {
|
||||
// Certificate is not valid at this time
|
||||
continue
|
||||
}
|
||||
|
||||
if !isSubjectInChain(subject, chain) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Select the most recently issued certificate. If there is a tie, pick
|
||||
// one arbitrarily.
|
||||
if len(bestCandidate.chain) > 0 && bestCandidate.chain[0].NotBefore.After(chain[0].NotBefore) {
|
||||
continue
|
||||
}
|
||||
|
||||
bestCandidate.id = id
|
||||
bestCandidate.chain = chain
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return bestCandidate.id, bestCandidate.chain
|
||||
}
|
||||
|
||||
// findIdentity locates an identity from the Windows or Darwin certificate
|
||||
@@ -105,7 +129,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
selected, chain := selectIdentityFromSlice(subject, ids)
|
||||
selected, chain := selectIdentityFromSlice(subject, ids, time.Now())
|
||||
|
||||
for _, id := range ids {
|
||||
if id != selected {
|
||||
|
||||
238
control/controlclient/sign_supported_test.go
Normal file
238
control/controlclient/sign_supported_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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.
|
||||
|
||||
//go:build windows && cgo
|
||||
// +build windows,cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
)
|
||||
|
||||
const (
|
||||
testRootCommonName = "testroot"
|
||||
testRootSubject = "CN=testroot"
|
||||
)
|
||||
|
||||
type testIdentity struct {
|
||||
chain []*x509.Certificate
|
||||
}
|
||||
|
||||
func makeChain(rootCommonName string, notBefore, notAfter time.Time) []*x509.Certificate {
|
||||
return []*x509.Certificate{
|
||||
{
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
PublicKeyAlgorithm: x509.RSA,
|
||||
},
|
||||
{
|
||||
Subject: pkix.Name{
|
||||
CommonName: rootCommonName,
|
||||
},
|
||||
PublicKeyAlgorithm: x509.RSA,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testIdentity) Certificate() (*x509.Certificate, error) {
|
||||
return t.chain[0], nil
|
||||
}
|
||||
|
||||
func (t *testIdentity) CertificateChain() ([]*x509.Certificate, error) {
|
||||
return t.chain, nil
|
||||
}
|
||||
|
||||
func (t *testIdentity) Signer() (crypto.Signer, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (t *testIdentity) Delete() error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (t *testIdentity) Close() {}
|
||||
|
||||
func TestSelectIdentityFromSlice(t *testing.T) {
|
||||
var times []time.Time
|
||||
for _, ts := range []string{
|
||||
"2000-01-01T00:00:00Z",
|
||||
"2001-01-01T00:00:00Z",
|
||||
"2002-01-01T00:00:00Z",
|
||||
"2003-01-01T00:00:00Z",
|
||||
} {
|
||||
tm, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
times = append(times, tm)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subject string
|
||||
ids []certstore.Identity
|
||||
now time.Time
|
||||
// wantIndex is an index into ids, or -1 for nil.
|
||||
wantIndex int
|
||||
}{
|
||||
{
|
||||
name: "single unexpired identity",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[2]),
|
||||
},
|
||||
},
|
||||
now: times[1],
|
||||
wantIndex: 0,
|
||||
},
|
||||
{
|
||||
name: "single expired identity",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: -1,
|
||||
},
|
||||
{
|
||||
name: "unrelated ids",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain("something", times[0], times[2]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[2]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain("else", times[0], times[2]),
|
||||
},
|
||||
},
|
||||
now: times[1],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "expired with unrelated ids",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain("something", times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain("else", times[0], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: -1,
|
||||
},
|
||||
{
|
||||
name: "one expired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "two certs both unexpired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "two unexpired one expired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotId, gotChain := selectIdentityFromSlice(tt.subject, tt.ids, tt.now)
|
||||
|
||||
if gotId == nil && gotChain != nil {
|
||||
t.Error("id is nil: got non-nil chain, want nil chain")
|
||||
return
|
||||
}
|
||||
if gotId != nil && gotChain == nil {
|
||||
t.Error("id is not nil: got nil chain, want non-nil chain")
|
||||
return
|
||||
}
|
||||
if tt.wantIndex == -1 {
|
||||
if gotId != nil {
|
||||
t.Error("got non-nil id, want nil id")
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotId == nil {
|
||||
t.Error("got nil id, want non-nil id")
|
||||
return
|
||||
}
|
||||
if gotId != tt.ids[tt.wantIndex] {
|
||||
found := -1
|
||||
for i := range tt.ids {
|
||||
if tt.ids[i] == gotId {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == -1 {
|
||||
t.Errorf("got unknown id, want id at index %v", tt.wantIndex)
|
||||
} else {
|
||||
t.Errorf("got id at index %v, want id at index %v", found, tt.wantIndex)
|
||||
}
|
||||
}
|
||||
|
||||
tid, ok := tt.ids[tt.wantIndex].(*testIdentity)
|
||||
if !ok {
|
||||
t.Error("got non-testIdentity, want testIdentity")
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tid.chain, gotChain) {
|
||||
t.Errorf("got unknown chain, want chain from id at index %v", tt.wantIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
242
control/controlhttp/client.go
Normal file
242
control/controlhttp/client.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlhttp implements the Tailscale 2021 control protocol
|
||||
// base transport over HTTP.
|
||||
//
|
||||
// This tunnels the protocol in control/controlbase over HTTP with a
|
||||
// variety of compatibility fallbacks for handling picky or deep
|
||||
// inspecting proxies.
|
||||
//
|
||||
// In the happy path, a client makes a single cleartext HTTP request
|
||||
// to the server, the server responds with 101 Switching Protocols,
|
||||
// and the control base protocol takes place over plain TCP.
|
||||
//
|
||||
// In the compatibility path, the client does the above over HTTPS,
|
||||
// resulting in double encryption (once for the control transport, and
|
||||
// once for the outer TLS layer).
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
const (
|
||||
upgradeHeaderValue = "tailscale-control-protocol"
|
||||
handshakeHeaderName = "X-Tailscale-Handshake"
|
||||
)
|
||||
|
||||
// Dial connects to the HTTP server at addr, requests to switch to the
|
||||
// Tailscale control protocol, and returns an established control
|
||||
// protocol connection.
|
||||
//
|
||||
// If Dial fails to connect using addr, it also tries to tunnel over
|
||||
// TLS to <addr's host>:443 as a compatibility fallback.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*controlbase.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &dialParams{
|
||||
ctx: ctx,
|
||||
host: host,
|
||||
httpPort: port,
|
||||
httpsPort: "443",
|
||||
machineKey: machineKey,
|
||||
controlKey: controlKey,
|
||||
proxyFunc: tshttpproxy.ProxyFromEnvironment,
|
||||
}
|
||||
return a.dial()
|
||||
}
|
||||
|
||||
type dialParams struct {
|
||||
ctx context.Context
|
||||
host string
|
||||
httpPort string
|
||||
httpsPort string
|
||||
machineKey key.MachinePrivate
|
||||
controlKey key.MachinePublic
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// For tests only
|
||||
insecureTLS bool
|
||||
}
|
||||
|
||||
func (a *dialParams) dial() (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: net.JoinHostPort(a.host, a.httpPort),
|
||||
Path: "/switch",
|
||||
}
|
||||
conn, httpErr := a.tryURL(u, init)
|
||||
if httpErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Connecting over plain HTTP failed, assume it's an HTTP proxy
|
||||
// being difficult and see if we can get through over HTTPS.
|
||||
u.Scheme = "https"
|
||||
u.Host = net.JoinHostPort(a.host, a.httpsPort)
|
||||
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, tlsErr := a.tryURL(u, init)
|
||||
if tlsErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr)
|
||||
}
|
||||
|
||||
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer(log.Printf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.proxyFunc
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dns)
|
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig)
|
||||
if a.insecureTLS {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
tr.TLSClientConfig.VerifyConnection = nil
|
||||
}
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dns, tr.TLSClientConfig)
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
// transport. We make exactly 1 request using this transport, so
|
||||
// there will be exactly 1 GotConn call. Additionally, the
|
||||
// transport handles 101 Switching Protocols correctly, such that
|
||||
// the Conn will not be reused or kept alive by the transport once
|
||||
// the response has been handed back from RoundTrip.
|
||||
//
|
||||
// In theory, the machinery of net/http should make it such that
|
||||
// the trace callback happens-before we get the response, but
|
||||
// there's no promise of that. So, to make sure, we use a buffered
|
||||
// channel as a synchronization step to avoid data races.
|
||||
//
|
||||
// Note that even though we're able to extract a net.Conn via this
|
||||
// mechanism, we must still keep using the eventual resp.Body to
|
||||
// read from, because it includes a buffer we can't get rid of. If
|
||||
// the server never sends any data after sending the HTTP
|
||||
// response, we could get away with it, but violating this
|
||||
// assumption leads to very mysterious transport errors (lockups,
|
||||
// unexpected EOFs...), and we're bound to forget someday and
|
||||
// introduce a protocol optimization at a higher level that starts
|
||||
// eagerly transmitting from the server.
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
connCh <- info.Conn
|
||||
},
|
||||
}
|
||||
ctx := httptrace.WithClientTrace(a.ctx, &trace)
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: u,
|
||||
Header: http.Header{
|
||||
"Upgrade": []string{upgradeHeaderValue},
|
||||
"Connection": []string{"upgrade"},
|
||||
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
},
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
return nil, fmt.Errorf("unexpected HTTP response: %s", resp.Status)
|
||||
}
|
||||
|
||||
// From here on, the underlying net.Conn is ours to use, but there
|
||||
// is still a read buffer attached to it within resp.Body. So, we
|
||||
// must direct I/O through resp.Body, but we can still use the
|
||||
// underlying net.Conn for stuff like deadlines.
|
||||
var switchedConn net.Conn
|
||||
select {
|
||||
case switchedConn = <-connCh:
|
||||
default:
|
||||
}
|
||||
if switchedConn == nil {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("httptrace didn't provide a connection")
|
||||
}
|
||||
|
||||
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
|
||||
}
|
||||
|
||||
rwc, ok := resp.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
resp.Body.Close()
|
||||
return nil, errors.New("http Transport did not provide a writable body")
|
||||
}
|
||||
|
||||
return &wrappedConn{switchedConn, rwc}, nil
|
||||
}
|
||||
|
||||
type wrappedConn struct {
|
||||
net.Conn
|
||||
rwc io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Read(bs []byte) (int, error) {
|
||||
return w.rwc.Read(bs)
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Write(bs []byte) (int, error) {
|
||||
return w.rwc.Write(bs)
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Close() error {
|
||||
return w.rwc.Close()
|
||||
}
|
||||
398
control/controlhttp/http_test.go
Normal file
398
control/controlhttp/http_test.go
Normal file
@@ -0,0 +1,398 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestControlHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proxy proxy
|
||||
}{
|
||||
// direct connection
|
||||
{
|
||||
name: "no_proxy",
|
||||
proxy: nil,
|
||||
},
|
||||
// SOCKS5
|
||||
{
|
||||
name: "socks5",
|
||||
proxy: &socksProxy{},
|
||||
},
|
||||
// HTTP->HTTP
|
||||
{
|
||||
name: "http_to_http",
|
||||
proxy: &httpProxy{
|
||||
useTLS: false,
|
||||
allowConnect: false,
|
||||
allowHTTP: true,
|
||||
},
|
||||
},
|
||||
// HTTP->HTTPS
|
||||
{
|
||||
name: "http_to_https",
|
||||
proxy: &httpProxy{
|
||||
useTLS: false,
|
||||
allowConnect: true,
|
||||
allowHTTP: false,
|
||||
},
|
||||
},
|
||||
// HTTP->any (will pick HTTP)
|
||||
{
|
||||
name: "http_to_any",
|
||||
proxy: &httpProxy{
|
||||
useTLS: false,
|
||||
allowConnect: true,
|
||||
allowHTTP: true,
|
||||
},
|
||||
},
|
||||
// HTTPS->HTTP
|
||||
{
|
||||
name: "https_to_http",
|
||||
proxy: &httpProxy{
|
||||
useTLS: true,
|
||||
allowConnect: false,
|
||||
allowHTTP: true,
|
||||
},
|
||||
},
|
||||
// HTTPS->HTTPS
|
||||
{
|
||||
name: "https_to_https",
|
||||
proxy: &httpProxy{
|
||||
useTLS: true,
|
||||
allowConnect: true,
|
||||
allowHTTP: false,
|
||||
},
|
||||
},
|
||||
// HTTPS->any (will pick HTTP)
|
||||
{
|
||||
name: "https_to_any",
|
||||
proxy: &httpProxy{
|
||||
useTLS: true,
|
||||
allowConnect: true,
|
||||
allowHTTP: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testControlHTTP(t, test.proxy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
sch := make(chan serverResult, 1)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := AcceptHTTP(context.Background(), w, r, server)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
res := serverResult{
|
||||
err: err,
|
||||
}
|
||||
if conn != nil {
|
||||
res.clientAddr = conn.RemoteAddr().String()
|
||||
res.version = conn.ProtocolVersion()
|
||||
res.peer = conn.Peer()
|
||||
res.conn = conn
|
||||
}
|
||||
sch <- res
|
||||
})
|
||||
|
||||
httpLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP listen: %v", err)
|
||||
}
|
||||
httpsLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
go httpServer.Serve(httpLn)
|
||||
defer httpServer.Close()
|
||||
|
||||
httpsServer := &http.Server{
|
||||
Handler: handler,
|
||||
TLSConfig: tlsConfig(t),
|
||||
}
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
defer httpsServer.Close()
|
||||
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
|
||||
a := dialParams{
|
||||
ctx: context.Background(), //ctx,
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
insecureTLS: true,
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
proxyEnv := proxy.Start(t)
|
||||
defer proxy.Close()
|
||||
proxyURL, err := url.Parse(proxyEnv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.proxyFunc = func(*http.Request) (*url.URL, error) {
|
||||
return proxyURL, nil
|
||||
}
|
||||
} else {
|
||||
a.proxyFunc = func(*http.Request) (*url.URL, error) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := a.dial()
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
si := <-sch
|
||||
if si.conn != nil {
|
||||
defer si.conn.Close()
|
||||
}
|
||||
if si.err != nil {
|
||||
t.Fatalf("controlhttp server got error: %v", err)
|
||||
}
|
||||
if clientVersion := conn.ProtocolVersion(); si.version != clientVersion {
|
||||
t.Fatalf("client and server don't agree on protocol version: %d vs %d", clientVersion, si.version)
|
||||
}
|
||||
if si.peer != client.Public() {
|
||||
t.Fatalf("server got peer pubkey %s, want %s", si.peer, client.Public())
|
||||
}
|
||||
if spub := conn.Peer(); spub != server.Public() {
|
||||
t.Fatalf("client got peer pubkey %s, want %s", spub, server.Public())
|
||||
}
|
||||
if proxy != nil && !proxy.ConnIsFromProxy(si.clientAddr) {
|
||||
t.Fatalf("client connected from %s, which isn't the proxy", si.clientAddr)
|
||||
}
|
||||
}
|
||||
|
||||
type serverResult struct {
|
||||
err error
|
||||
clientAddr string
|
||||
version int
|
||||
peer key.MachinePublic
|
||||
conn *controlbase.Conn
|
||||
}
|
||||
|
||||
type proxy interface {
|
||||
Start(*testing.T) string
|
||||
Close()
|
||||
ConnIsFromProxy(string) bool
|
||||
}
|
||||
|
||||
type socksProxy struct {
|
||||
sync.Mutex
|
||||
proxy socks5.Server
|
||||
ln net.Listener
|
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
}
|
||||
|
||||
func (s *socksProxy) Start(t *testing.T) (url string) {
|
||||
t.Helper()
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listening for SOCKS server: %v", err)
|
||||
}
|
||||
s.ln = ln
|
||||
s.clientConnAddrs = map[string]bool{}
|
||||
s.proxy.Logf = t.Logf
|
||||
s.proxy.Dialer = s.dialAndRecord
|
||||
go s.proxy.Serve(ln)
|
||||
return fmt.Sprintf("socks5://%s", ln.Addr().String())
|
||||
}
|
||||
|
||||
func (s *socksProxy) Close() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.ln.Close()
|
||||
}
|
||||
|
||||
func (s *socksProxy) dialAndRecord(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.clientConnAddrs[conn.LocalAddr().String()] = true
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (s *socksProxy) ConnIsFromProxy(addr string) bool {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.clientConnAddrs[addr]
|
||||
}
|
||||
|
||||
type httpProxy struct {
|
||||
useTLS bool // take incoming connections over TLS
|
||||
allowConnect bool // allow CONNECT for TLS
|
||||
allowHTTP bool // allow plain HTTP proxying
|
||||
|
||||
sync.Mutex
|
||||
ln net.Listener
|
||||
rp httputil.ReverseProxy
|
||||
s http.Server
|
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
}
|
||||
|
||||
func (h *httpProxy) Start(t *testing.T) (url string) {
|
||||
t.Helper()
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listening for HTTP proxy: %v", err)
|
||||
}
|
||||
h.ln = ln
|
||||
h.rp = httputil.ReverseProxy{
|
||||
Director: func(*http.Request) {},
|
||||
Transport: &http.Transport{
|
||||
DialContext: h.dialAndRecord,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
|
||||
},
|
||||
}
|
||||
h.clientConnAddrs = map[string]bool{}
|
||||
h.s.Handler = h
|
||||
if h.useTLS {
|
||||
h.s.TLSConfig = tlsConfig(t)
|
||||
go h.s.ServeTLS(h.ln, "", "")
|
||||
return fmt.Sprintf("https://%s", ln.Addr().String())
|
||||
} else {
|
||||
go h.s.Serve(h.ln)
|
||||
return fmt.Sprintf("http://%s", ln.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpProxy) Close() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.s.Close()
|
||||
}
|
||||
|
||||
func (h *httpProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "CONNECT" {
|
||||
if !h.allowHTTP {
|
||||
http.Error(w, "http proxy not allowed", 500)
|
||||
return
|
||||
}
|
||||
h.rp.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.allowConnect {
|
||||
http.Error(w, "connect not allowed", 500)
|
||||
return
|
||||
}
|
||||
|
||||
dst := r.RequestURI
|
||||
c, err := h.dialAndRecord(context.Background(), "tcp", dst)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
cc, ccbuf, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(cc, c)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(c, ccbuf)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
}
|
||||
|
||||
func (h *httpProxy) dialAndRecord(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.clientConnAddrs[conn.LocalAddr().String()] = true
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (h *httpProxy) ConnIsFromProxy(addr string) bool {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
return h.clientConnAddrs[addr]
|
||||
}
|
||||
|
||||
func tlsConfig(t *testing.T) *tls.Config {
|
||||
// Cert and key taken from the example code in the crypto/tls
|
||||
// package.
|
||||
certPem := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||
6MF9+Yw1Yy0t
|
||||
-----END CERTIFICATE-----`)
|
||||
keyPem := []byte(`-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
|
||||
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
|
||||
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||
-----END EC PRIVATE KEY-----`)
|
||||
cert, err := tls.X509KeyPair(certPem, keyPem)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
95
control/controlhttp/server.go
Normal file
95
control/controlhttp/server.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// AcceptHTTP upgrades the HTTP request given by w and r into a
|
||||
// Tailscale control protocol base transport connection.
|
||||
//
|
||||
// AcceptHTTP always writes an HTTP response to w. The caller must not
|
||||
// attempt their own response after calling AcceptHTTP.
|
||||
func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
|
||||
next := r.Header.Get("Upgrade")
|
||||
if next == "" {
|
||||
http.Error(w, "missing next protocol", http.StatusBadRequest)
|
||||
return nil, errors.New("no next protocol in HTTP request")
|
||||
}
|
||||
if next != upgradeHeaderValue {
|
||||
http.Error(w, "unknown next protocol", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
|
||||
}
|
||||
|
||||
initB64 := r.Header.Get(handshakeHeaderName)
|
||||
if initB64 == "" {
|
||||
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
|
||||
return nil, errors.New("no tailscale handshake header in HTTP request")
|
||||
}
|
||||
init, err := base64.StdEncoding.DecodeString(initB64)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("decoding base64 handshake header: %v", err)
|
||||
}
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
|
||||
return nil, errors.New("can't hijack client connection")
|
||||
}
|
||||
|
||||
w.Header().Set("Upgrade", upgradeHeaderValue)
|
||||
w.Header().Set("Connection", "upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
|
||||
conn, brw, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hijacking client connection: %w", err)
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
|
||||
}
|
||||
if brw.Reader.Buffered() > 0 {
|
||||
conn = &drainBufConn{conn, brw.Reader}
|
||||
}
|
||||
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("noise handshake failed: %w", err)
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// drainBufConn is a net.Conn with an initial bunch of bytes in a
|
||||
// bufio.Reader. Read drains the bufio.Reader until empty, then passes
|
||||
// through subsequent reads to the Conn directly.
|
||||
type drainBufConn struct {
|
||||
net.Conn
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func (b *drainBufConn) Read(bs []byte) (int, error) {
|
||||
if b.r == nil {
|
||||
return b.Conn.Read(bs)
|
||||
}
|
||||
n, err := b.r.Read(bs)
|
||||
if b.r.Buffered() == 0 {
|
||||
b.r = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -7,9 +7,7 @@
|
||||
package controlknobs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
@@ -17,8 +15,7 @@ import (
|
||||
var disableUPnP syncs.AtomicBool
|
||||
|
||||
func init() {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DISABLE_UPNP"))
|
||||
SetDisableUPnP(v)
|
||||
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
|
||||
}
|
||||
|
||||
// DisableUPnP reports the last reported value from control
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -37,8 +39,8 @@ type Client struct {
|
||||
rate *rate.Limiter // if non-nil, rate limiter to use
|
||||
|
||||
// Owned by Recv:
|
||||
peeked int // bytes to discard on next Recv
|
||||
readErr error // sticky read error
|
||||
peeked int // bytes to discard on next Recv
|
||||
readErr atomic.Value // of error; sticky (set by Recv)
|
||||
}
|
||||
|
||||
// ClientOpt is an option passed to NewClient.
|
||||
@@ -261,10 +263,18 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPing(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePing, data)
|
||||
}
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePong, data)
|
||||
}
|
||||
|
||||
func (c *Client) sendPingOrPong(typ frameType, data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
|
||||
if err := writeFrameHeader(c.bw, typ, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
@@ -375,6 +385,12 @@ type PingMessage [8]byte
|
||||
|
||||
func (PingMessage) msg() {}
|
||||
|
||||
// PongMessage is a reply to a PingMessage from a client or server
|
||||
// with the payload sent previously in a PingMessage.
|
||||
type PongMessage [8]byte
|
||||
|
||||
func (PongMessage) msg() {}
|
||||
|
||||
// KeepAliveMessage is a one-way empty message from server to client, just to
|
||||
// keep the connection alive. It's like a PingMessage, but doesn't solicit
|
||||
// a reply from the client.
|
||||
@@ -427,13 +443,14 @@ func (c *Client) Recv() (m ReceivedMessage, err error) {
|
||||
}
|
||||
|
||||
func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err error) {
|
||||
if c.readErr != nil {
|
||||
return nil, c.readErr
|
||||
readErr, _ := c.readErr.Load().(error)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.Recv: %w", err)
|
||||
c.readErr = err
|
||||
c.readErr.Store(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -536,6 +553,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case framePong:
|
||||
var pm PongMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
continue
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case frameHealth:
|
||||
return HealthMessage{Problem: string(b[:])}, nil
|
||||
|
||||
@@ -564,3 +590,22 @@ func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
|
||||
sm.TokenBucketBytesBurst)
|
||||
}
|
||||
}
|
||||
|
||||
// LocalAddr returns the TCP connection's local address.
|
||||
//
|
||||
// If the client is broken in some previously detectable way, it
|
||||
// returns an error.
|
||||
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
|
||||
readErr, _ := c.readErr.Load().(error)
|
||||
if readErr != nil {
|
||||
return netaddr.IPPort{}, readErr
|
||||
}
|
||||
if c.nc == nil {
|
||||
return netaddr.IPPort{}, errors.New("nil conn")
|
||||
}
|
||||
a := c.nc.LocalAddr()
|
||||
if a == nil {
|
||||
return netaddr.IPPort{}, errors.New("nil addr")
|
||||
}
|
||||
return netaddr.ParseIPPort(a.String())
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/key"
|
||||
@@ -47,14 +48,14 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
|
||||
var debug = envknob.Bool("DERP_DEBUG_LOGS")
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.NodePublic]bool{}
|
||||
|
||||
func init() {
|
||||
keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
|
||||
keys := envknob.String("TS_DEBUG_VERBOSE_DROPS")
|
||||
if keys == "" {
|
||||
return
|
||||
}
|
||||
@@ -124,6 +125,8 @@ type Server struct {
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
gotPing expvar.Int // number of ping frames from client
|
||||
sentPong expvar.Int // number of pong frames enqueued to client
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
@@ -283,9 +286,8 @@ type PacketForwarder interface {
|
||||
// It is a defined type so that non-net connections can be used.
|
||||
type Conn interface {
|
||||
io.WriteCloser
|
||||
|
||||
LocalAddr() net.Addr
|
||||
// The *Deadline methods follow the semantics of net.Conn.
|
||||
|
||||
SetDeadline(time.Time) error
|
||||
SetReadDeadline(time.Time) error
|
||||
SetWriteDeadline(time.Time) error
|
||||
@@ -662,6 +664,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
sendPongCh: make(chan [8]byte, 1),
|
||||
peerGone: make(chan key.NodePublic),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
}
|
||||
@@ -729,6 +732,8 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
err = c.handleFrameWatchConns(ft, fl)
|
||||
case frameClosePeer:
|
||||
err = c.handleFrameClosePeer(ft, fl)
|
||||
case framePing:
|
||||
err = c.handleFramePing(ft, fl)
|
||||
default:
|
||||
err = c.handleUnknownFrame(ft, fl)
|
||||
}
|
||||
@@ -766,6 +771,33 @@ func (c *sclient) handleFrameWatchConns(ft frameType, fl uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
|
||||
c.s.gotPing.Add(1)
|
||||
var m PingMessage
|
||||
if fl < uint32(len(m)) {
|
||||
return fmt.Errorf("short ping: %v", fl)
|
||||
}
|
||||
if fl > 1000 {
|
||||
// unreasonably extra large. We leave some extra
|
||||
// space for future extensibility, but not too much.
|
||||
return fmt.Errorf("ping body too large: %v", fl)
|
||||
}
|
||||
_, err := io.ReadFull(c.br, m[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if extra := int64(fl) - int64(len(m)); extra > 0 {
|
||||
_, err = io.CopyN(ioutil.Discard, c.br, extra)
|
||||
}
|
||||
select {
|
||||
case c.sendPongCh <- [8]byte(m):
|
||||
default:
|
||||
// They're pinging too fast. Ignore.
|
||||
// TODO(bradfitz): add a rate limiter too.
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
|
||||
if fl != keyLen {
|
||||
return fmt.Errorf("handleFrameClosePeer wrong size")
|
||||
@@ -1202,6 +1234,7 @@ type sclient struct {
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
sendPongCh chan [8]byte // pong replies to send to the client; never closed
|
||||
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
@@ -1342,6 +1375,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case msg := <-c.sendPongCh:
|
||||
werr = c.sendPong(msg)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
continue
|
||||
@@ -1368,6 +1404,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case msg := <-c.sendPongCh:
|
||||
werr = c.sendPong(msg)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
}
|
||||
@@ -1384,6 +1423,17 @@ func (c *sclient) sendKeepAlive() error {
|
||||
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
|
||||
}
|
||||
|
||||
// sendPong sends a pong reply, without flushing.
|
||||
func (c *sclient) sendPong(data [8]byte) error {
|
||||
c.s.sentPong.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePong, uint32(len(data))); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(data[:])
|
||||
return err
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
@@ -1625,6 +1675,8 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("unknown_frames", &s.unknownFrames)
|
||||
m.Set("home_moves_in", &s.homeMovesIn)
|
||||
m.Set("home_moves_out", &s.homeMovesOut)
|
||||
m.Set("got_ping", &s.gotPing)
|
||||
m.Set("sent_pong", &s.sentPong)
|
||||
m.Set("peer_gone_frames", &s.peerGoneFrames)
|
||||
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
|
||||
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
|
||||
|
||||
@@ -812,6 +812,14 @@ func TestClientRecv(t *testing.T) {
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
input: []byte{
|
||||
byte(framePong), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
},
|
||||
want: PongMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
{
|
||||
name: "health_bad",
|
||||
input: []byte{
|
||||
@@ -858,6 +866,23 @@ func TestClientRecv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPing(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(&buf),
|
||||
}
|
||||
if err := c.SendPing([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{
|
||||
byte(framePing), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPong(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
@@ -873,7 +898,6 @@ func TestClientSendPong(t *testing.T) {
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServerDupClients(t *testing.T) {
|
||||
@@ -1316,3 +1340,30 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
t.Errorf("limited conn's bytes count = %v; want >=%v, <%v", bytesLimited, bytes1K*2, bytes1K)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRepliesToPing(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.close(t)
|
||||
|
||||
tc := newRegularClient(t, ts, "alice")
|
||||
|
||||
data := [8]byte{1, 2, 3, 4, 5, 6, 7, 42}
|
||||
|
||||
if err := tc.c.SendPing(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PongMessage:
|
||||
if ([8]byte(m)) != data {
|
||||
t.Fatalf("got pong %2x; want %2x", [8]byte(m), data)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ package derphttp
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@@ -22,16 +23,16 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
@@ -64,6 +65,12 @@ type Client struct {
|
||||
ctx context.Context // closed via cancelCtx in Client.Close
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
// addrFamSelAtomic is the last AddressFamilySelector set
|
||||
// by SetAddressFamilySelector. It's an atomic because it needs
|
||||
// to be accessed by multiple racing routines started while
|
||||
// Client.conn holds mu.
|
||||
addrFamSelAtomic atomic.Value // of AddressFamilySelector
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
@@ -72,6 +79,8 @@ type Client struct {
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.NodePublic
|
||||
tlsState *tls.ConnectionState
|
||||
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
|
||||
}
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
@@ -123,6 +132,17 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TLSConnectionState returns the last TLS connection state, if any.
|
||||
// The client must already be connected.
|
||||
func (c *Client) TLSConnectionState() (_ *tls.ConnectionState, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed || c.client == nil {
|
||||
return nil, false
|
||||
}
|
||||
return c.tlsState, c.tlsState != nil
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
//
|
||||
// It only returns a non-zero value once a connection has succeeded
|
||||
@@ -180,6 +200,32 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// AddressFamilySelector decides whethers IPv6 is preferred for
|
||||
// outbound dials.
|
||||
type AddressFamilySelector interface {
|
||||
// PreferIPv6 reports whether IPv4 dials should be slightly
|
||||
// delayed to give IPv6 a better chance of winning dial races.
|
||||
// Implementations should only return true if IPv6 is expected
|
||||
// to succeed. (otherwise delaying IPv4 will delay the
|
||||
// connection overall)
|
||||
PreferIPv6() bool
|
||||
}
|
||||
|
||||
// SetAddressFamilySelector sets the AddressFamilySelector that this
|
||||
// connection will use. It should be called before any dials.
|
||||
// The value must not be nil. If called more than once, s must
|
||||
// be the same concrete type as any prior calls.
|
||||
func (c *Client) SetAddressFamilySelector(s AddressFamilySelector) {
|
||||
c.addrFamSelAtomic.Store(s)
|
||||
}
|
||||
|
||||
func (c *Client) preferIPv6() bool {
|
||||
if s, ok := c.addrFamSelAtomic.Load().(AddressFamilySelector); ok {
|
||||
return s.PreferIPv6()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dialWebsocketFunc is non-nil (set by websocket.go's init) when compiled in.
|
||||
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
|
||||
|
||||
@@ -188,8 +234,7 @@ func useWebsockets() bool {
|
||||
return true
|
||||
}
|
||||
if dialWebsocketFunc != nil {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DERP_WS_CLIENT"))
|
||||
return v
|
||||
return envknob.Bool("TS_DEBUG_DERP_WS_CLIENT")
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -318,6 +363,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.NodePublic // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var serverProtoVersion int
|
||||
var tlsState *tls.ConnectionState
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
httpConn = tlsConn
|
||||
@@ -340,9 +386,10 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
// Note that we're not specifically concerned about TLS downgrade
|
||||
// attacks. TLS handles that fine:
|
||||
// https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html
|
||||
connState := tlsConn.ConnectionState()
|
||||
if connState.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates)
|
||||
cs := tlsConn.ConnectionState()
|
||||
tlsState = &cs
|
||||
if cs.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(cs.PeerCertificates)
|
||||
}
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
@@ -409,6 +456,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.tlsState = tlsState
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
@@ -429,7 +477,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
|
||||
}
|
||||
hostOrIP := host
|
||||
dialer := netns.NewDialer()
|
||||
dialer := netns.NewDialer(c.logf)
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
@@ -519,7 +567,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
|
||||
}
|
||||
|
||||
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
|
||||
return netns.NewDialer().DialContext(ctx, proto, addr)
|
||||
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
|
||||
}
|
||||
|
||||
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
|
||||
@@ -568,6 +616,18 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
nwait++
|
||||
go func() {
|
||||
if proto == "tcp4" && c.preferIPv6() {
|
||||
t := time.NewTimer(200 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Either user canceled original context,
|
||||
// it timed out, or the v6 dial succeeded.
|
||||
t.Stop()
|
||||
return
|
||||
case <-t.C:
|
||||
// Start v4 dial
|
||||
}
|
||||
}
|
||||
dst := dstPrimary
|
||||
if dst == "" {
|
||||
dst = n.HostName
|
||||
@@ -698,6 +758,95 @@ func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) registerPing(m derp.PingMessage, ch chan<- bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.pingOut == nil {
|
||||
c.pingOut = map[derp.PingMessage]chan<- bool{}
|
||||
}
|
||||
c.pingOut[m] = ch
|
||||
}
|
||||
|
||||
func (c *Client) unregisterPing(m derp.PingMessage) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.pingOut, m)
|
||||
}
|
||||
|
||||
func (c *Client) handledPong(m derp.PongMessage) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
k := derp.PingMessage(m)
|
||||
if ch, ok := c.pingOut[k]; ok {
|
||||
ch <- true
|
||||
delete(c.pingOut, k)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Ping sends a ping to the peer and waits for it either to be
|
||||
// acknowledged (in which case Ping returns nil) or waits for ctx to
|
||||
// be over and returns an error. It will wait at most 5 seconds
|
||||
// before returning an error.
|
||||
//
|
||||
// Another goroutine must be in a loop calling Recv or
|
||||
// RecvDetail or ping responses won't be handled.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
maxDL := time.Now().Add(5 * time.Second)
|
||||
if dl, ok := ctx.Deadline(); !ok || dl.After(maxDL) {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, maxDL)
|
||||
defer cancel()
|
||||
}
|
||||
var data derp.PingMessage
|
||||
rand.Read(data[:])
|
||||
gotPing := make(chan bool, 1)
|
||||
c.registerPing(data, gotPing)
|
||||
defer c.unregisterPing(data)
|
||||
if err := c.SendPing(data); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-gotPing:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// SendPing writes a ping message, without any implicit connect or
|
||||
// reconnect. This is a lower-level interface that writes a frame
|
||||
// without any implicit handling of the response pong, if any. For a
|
||||
// higher-level interface, use Ping.
|
||||
func (c *Client) SendPing(data [8]byte) error {
|
||||
c.mu.Lock()
|
||||
closed, client := c.closed, c.client
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return ErrClientClosed
|
||||
}
|
||||
if client == nil {
|
||||
return errors.New("client not connected")
|
||||
}
|
||||
return client.SendPing(data)
|
||||
}
|
||||
|
||||
// LocalAddr reports c's local TCP address, without any implicit
|
||||
// connect or reconnect.
|
||||
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
|
||||
c.mu.Lock()
|
||||
closed, client := c.closed, c.client
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return netaddr.IPPort{}, ErrClientClosed
|
||||
}
|
||||
if client == nil {
|
||||
return netaddr.IPPort{}, errors.New("client not connected")
|
||||
}
|
||||
return client.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
@@ -805,14 +954,22 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
for {
|
||||
m, err = client.Recv()
|
||||
switch m := m.(type) {
|
||||
case derp.PongMessage:
|
||||
if c.handledPong(m) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
}
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
|
||||
@@ -154,3 +154,55 @@ func waitConnect(t testing.TB, c *Client) {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := derp.NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
|
||||
httpsrv := &http.Server{
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: Handler(s),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp4", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverURL := "http://" + ln.Addr().String()
|
||||
t.Logf("server URL: %s", serverURL)
|
||||
|
||||
go func() {
|
||||
if err := httpsrv.Serve(ln); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
if err := c.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("client Connect: %v", err)
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
t.Logf("Recv: %T", m)
|
||||
}
|
||||
}()
|
||||
err = c.Ping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Ping: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
144
envknob/envknob.go
Normal file
144
envknob/envknob.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 envknob provides access to environment-variable tweakable
|
||||
// debug settings.
|
||||
//
|
||||
// These are primarily knobs used by Tailscale developers during
|
||||
// development or by users when instructed to by Tailscale developers
|
||||
// when debugging something. They are not a stable interface and may
|
||||
// be removed or any time.
|
||||
//
|
||||
// A related package, control/controlknobs, are knobs that can be
|
||||
// changed at runtime by the control plane. Sometimes both are used:
|
||||
// an envknob for the default/explicit value, else falling back
|
||||
// to the controlknob value.
|
||||
package envknob
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
list []string
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, ok := set[v]; !ok {
|
||||
list = append(list, k)
|
||||
}
|
||||
set[k] = v
|
||||
}
|
||||
|
||||
// logf is logger.Logf, but logger depends on envknob, so for circular
|
||||
// dependency reasons, make a type alias (so it's still assignable,
|
||||
// but has nice docs here).
|
||||
type logf = func(format string, args ...interface{})
|
||||
|
||||
// LogCurrent logs the currently set environment knobs.
|
||||
func LogCurrent(logf logf) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, k := range list {
|
||||
logf("envknob: %s=%q", k, set[k])
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the named environment variable, using os.Getenv.
|
||||
//
|
||||
// If the variable is non-empty, it's also tracked & logged as being
|
||||
// an in-use knob.
|
||||
func String(envVar string) string {
|
||||
v := os.Getenv(envVar)
|
||||
noteEnv(envVar, v)
|
||||
return v
|
||||
}
|
||||
|
||||
// Bool returns the boolean value of the named environment variable.
|
||||
// If the variable is not set, it returns false.
|
||||
// An invalid value exits the binary with a failure.
|
||||
func Bool(envVar string) bool {
|
||||
return boolOr(envVar, false)
|
||||
}
|
||||
|
||||
// BoolDefaultTrue is like Bool, but returns true by default if the
|
||||
// environment variable isn't present.
|
||||
func BoolDefaultTrue(envVar string) bool {
|
||||
return boolOr(envVar, true)
|
||||
}
|
||||
|
||||
func boolOr(envVar string, implicitValue bool) bool {
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return implicitValue
|
||||
}
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err == nil {
|
||||
noteEnv(envVar, strconv.FormatBool(b)) // canonicalize
|
||||
return b
|
||||
}
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// LookupBool returns the boolean value of the named environment value.
|
||||
// The ok result is whether a value was set.
|
||||
// If the value isn't a valid int, it exits the program with a failure.
|
||||
func LookupBool(envVar string) (v bool, ok bool) {
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return false, false
|
||||
}
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err == nil {
|
||||
return b, true
|
||||
}
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// OptBool is like Bool, but returns an opt.Bool, so the caller can
|
||||
// distinguish between implicitly and explicitly false.
|
||||
func OptBool(envVar string) opt.Bool {
|
||||
b, ok := LookupBool(envVar)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var ret opt.Bool
|
||||
ret.Set(b)
|
||||
return ret
|
||||
}
|
||||
|
||||
// LookupInt returns the integer value of the named environment value.
|
||||
// The ok result is whether a value was set.
|
||||
// If the value isn't a valid int, it exits the program with a failure.
|
||||
func LookupInt(envVar string) (v int, ok bool) {
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return 0, false
|
||||
}
|
||||
v, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
noteEnv(envVar, val)
|
||||
return v, true
|
||||
}
|
||||
log.Fatalf("invalid integer environment variable %s: %v", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// UseWIPCode is whether TAILSCALE_USE_WIP_CODE is set to permit use
|
||||
// of Work-In-Progress code.
|
||||
func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
|
||||
251
go.mod
251
go.mod
@@ -7,208 +7,257 @@ require (
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.8.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.11.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/frankban/quicktest v1.14.0
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
|
||||
github.com/godbus/dbus/v5 v5.0.5
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211026125128-ad197bcd36fd
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.13.6
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/netlink v1.4.2
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.43
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/pborman/getopt v1.1.0
|
||||
github.com/peterbourgon/ff/v3 v3.1.0
|
||||
github.com/peterbourgon/ff/v3 v3.1.2
|
||||
github.com/pkg/sftp v1.13.4
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
||||
golang.org/x/tools v0.1.7
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.8
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
honnef.co/go/tools v0.2.1
|
||||
gvisor.dev/gvisor v0.0.0-20220126021142-d8aa030b2591
|
||||
honnef.co/go/tools v0.2.2
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||
github.com/Antonboom/errname v0.1.5 // indirect
|
||||
github.com/Antonboom/nilnil v0.1.0 // indirect
|
||||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 // indirect
|
||||
github.com/aws/smithy-go v1.8.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.2.0 // indirect
|
||||
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.0 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
|
||||
github.com/blizzy78/varnamelen v0.5.0 // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
|
||||
github.com/breml/bidichk v0.2.1 // indirect
|
||||
github.com/butuzov/ireturn v0.1.1 // indirect
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||
github.com/daixiang0/gci v0.2.7 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.3.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-critic/go-critic v0.5.2 // indirect
|
||||
github.com/esimonov/ifshort v1.0.3 // indirect
|
||||
github.com/ettle/strcase v0.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fzipp/gocyclo v0.3.1 // indirect
|
||||
github.com/go-critic/go-critic v0.6.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.0.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.2.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.1 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.0.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.0.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.0.2 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
|
||||
github.com/golangci/golangci-lint v1.33.0 // indirect
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc // indirect
|
||||
github.com/golangci/golangci-lint v1.43.0 // indirect
|
||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
|
||||
github.com/golangci/misspell v0.3.5 // indirect
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
github.com/goreleaser/chglog v0.1.2 // indirect
|
||||
github.com/goreleaser/fileglob v0.3.1 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.6.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.1 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 // indirect
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect
|
||||
github.com/jgautheron/goconst v1.5.1 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
|
||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||
github.com/kisielk/errcheck v1.6.0 // indirect
|
||||
github.com/kisielk/gotool v1.0.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.2 // indirect
|
||||
github.com/kulti/thelper v0.4.0 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.3 // indirect
|
||||
github.com/kyoh86/exportloopref v0.1.8 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/ldez/gomoddirectives v0.2.2 // indirect
|
||||
github.com/ldez/tagliatelle v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/maratori/testpackage v1.0.1 // indirect
|
||||
github.com/matoous/godox v0.0.0-20200801072554-4fb83dc2941e // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.1.0 // indirect
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect
|
||||
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
|
||||
github.com/mgechev/revive v1.1.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moricho/tparallel v0.2.1 // indirect
|
||||
github.com/nakabonne/nestif v0.3.0 // indirect
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
|
||||
github.com/nishanths/exhaustive v0.1.0 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
|
||||
github.com/nishanths/exhaustive v0.7.11 // indirect
|
||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 // indirect
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.2.1 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c // indirect
|
||||
github.com/rogpeppe/go-internal v1.6.2 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.1.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.3.13 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.2.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
|
||||
github.com/securego/gosec/v2 v2.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.9.3 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sivchari/tenv v1.4.7 // indirect
|
||||
github.com/sonatard/noctx v0.0.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.6.1 // indirect
|
||||
github.com/spf13/afero v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.1.1 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.2.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
|
||||
github.com/spf13/viper v1.9.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect
|
||||
github.com/tetafro/godot v1.3.2 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 // indirect
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa // indirect
|
||||
github.com/sylvia7788/contextcheck v1.0.4 // indirect
|
||||
github.com/tdakkota/asciicheck v0.1.1 // indirect
|
||||
github.com/tetafro/godot v1.4.11 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
|
||||
github.com/ultraware/funlen v0.0.3 // indirect
|
||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
||||
github.com/uudashr/gocognit v1.0.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/uudashr/gocognit v1.0.5 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
mvdan.cc/gofumpt v0.2.0 // indirect
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 // indirect
|
||||
)
|
||||
|
||||
1
go.toolchain.branch
Normal file
1
go.toolchain.branch
Normal file
@@ -0,0 +1 @@
|
||||
tailscale.go1.17
|
||||
1
go.toolchain.rev
Normal file
1
go.toolchain.rev
Normal file
@@ -0,0 +1 @@
|
||||
25fe91a25c9630a50138a135105af19ae7c7c3e7
|
||||
@@ -9,13 +9,14 @@ package health
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
@@ -28,6 +29,8 @@ var (
|
||||
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
|
||||
timer *time.Timer
|
||||
|
||||
debugHandler = map[string]http.Handler{}
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
lastMapPollEndedAt time.Time
|
||||
@@ -58,6 +61,9 @@ const (
|
||||
// SysDNS is the name of the net/dns subsystem.
|
||||
SysDNS = Subsystem("dns")
|
||||
|
||||
// SysDNSOS is the name of the net/dns OSConfigurator subsystem.
|
||||
SysDNSOS = Subsystem("dns-os")
|
||||
|
||||
// SysNetworkCategory is the name of the subsystem that sets
|
||||
// the Windows network adapter's "category" (public, private, domain).
|
||||
// If it's unhealthy, the Windows firewall rules won't match.
|
||||
@@ -101,12 +107,30 @@ func SetDNSHealth(err error) { set(SysDNS, err) }
|
||||
// DNSHealth returns the net/dns.Manager error state.
|
||||
func DNSHealth() error { return get(SysDNS) }
|
||||
|
||||
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator
|
||||
func SetDNSOSHealth(err error) { set(SysDNSOS, err) }
|
||||
|
||||
// DNSOSHealth returns the net/dns.OSConfigurator error state.
|
||||
func DNSOSHealth() error { return get(SysDNSOS) }
|
||||
|
||||
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
|
||||
// This only applies on Windows.
|
||||
func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
|
||||
|
||||
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
|
||||
|
||||
func RegisterDebugHandler(typ string, h http.Handler) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
debugHandler[typ] = h
|
||||
}
|
||||
|
||||
func DebugHandler(typ string) http.Handler {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return debugHandler[typ]
|
||||
}
|
||||
|
||||
func get(key Subsystem) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -159,7 +183,8 @@ func GotStreamedMapResponse() {
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetInPollNetMap records that we're in
|
||||
// SetInPollNetMap records whether the client has an open
|
||||
// HTTP long poll open to the control plane.
|
||||
func SetInPollNetMap(v bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -174,6 +199,14 @@ func SetInPollNetMap(v bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetInPollNetMap reports whether the client has an open
|
||||
// HTTP long poll open to the control plane.
|
||||
func GetInPollNetMap() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return inMapPoll
|
||||
}
|
||||
|
||||
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
|
||||
func SetMagicSockDERPHome(region int) {
|
||||
mu.Lock()
|
||||
@@ -275,7 +308,7 @@ func OverallError() error {
|
||||
return overallErrorLocked()
|
||||
}
|
||||
|
||||
var fakeErrForTesting = os.Getenv("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
|
||||
func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
|
||||
@@ -87,6 +87,7 @@ const (
|
||||
AWSFargate = EnvType("fg")
|
||||
FlyDotIo = EnvType("fly")
|
||||
Kubernetes = EnvType("k8s")
|
||||
DockerDesktop = EnvType("dde")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -144,6 +145,9 @@ func getEnvType() EnvType {
|
||||
if inKubernetes() {
|
||||
return Kubernetes
|
||||
}
|
||||
if inDockerDesktop() {
|
||||
return DockerDesktop
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -228,6 +232,13 @@ func inKubernetes() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func inDockerDesktop() bool {
|
||||
if os.Getenv("TS_HOST_ENV") == "dde" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type etcAptSrcResult struct {
|
||||
mod time.Time
|
||||
disabled bool
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -21,19 +22,37 @@ func osVersionWindows() string {
|
||||
if s, ok := winVerCache.Load().(string); ok {
|
||||
return s
|
||||
}
|
||||
cmd := exec.Command("cmd", "/c", "ver")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
|
||||
s := strings.TrimSpace(string(out))
|
||||
s = strings.TrimPrefix(s, "Microsoft Windows [")
|
||||
s = strings.TrimSuffix(s, "]")
|
||||
|
||||
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
|
||||
if sp := strings.Index(s, " "); sp != -1 {
|
||||
s = s[sp+1:]
|
||||
major, minor, build := windows.RtlGetNtVersionNumbers()
|
||||
s := fmt.Sprintf("%d.%d.%d", major, minor, build)
|
||||
// Windows 11 still uses 10 as its major number internally
|
||||
if major == 10 {
|
||||
if ubr, err := getUBR(); err == nil {
|
||||
s += fmt.Sprintf(".%d", ubr)
|
||||
}
|
||||
}
|
||||
if s != "" {
|
||||
winVerCache.Store(s)
|
||||
}
|
||||
return s // "10.0.19041.388", ideally
|
||||
}
|
||||
|
||||
// getUBR obtains a fourth version field, the "Update Build Revision",
|
||||
// from the registry. This field is only available beginning with Windows 10.
|
||||
func getUBR() (uint32, error) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE|registry.WOW64_64KEY)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
val, valType, err := key.GetIntegerValue("UBR")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if valType != registry.DWORD {
|
||||
return 0, registry.ErrUnexpectedType
|
||||
}
|
||||
|
||||
return uint32(val), nil
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
OnlyIPv6: true,
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"b.net.": ips("fe75::2"),
|
||||
"myname.net.": ips("fe75::1"),
|
||||
@@ -232,32 +233,11 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "android_does_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
Routes: map[string][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
CorpDNS: true,
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4:53"},
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4:53"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
|
||||
// Android had cases where it needed FallbackResolvers. This was the
|
||||
// negative test for the case where Override-local-DNS was set, so the
|
||||
// fallback resolvers did not need to be used. This test is still valid
|
||||
// so we keep it, but the fallback test has been removed.
|
||||
name: "android_does_NOT_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
@@ -344,3 +324,48 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
|
||||
b := &LocalBackend{}
|
||||
if b.allowExitNodeDNSProxyToServeName("google.com") {
|
||||
t.Fatal("unexpected true on backend with nil NetMap")
|
||||
}
|
||||
|
||||
b.netMap = &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
ExitNodeFilteredSet: []string{
|
||||
".ts.net",
|
||||
"some.exact.bad",
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
// Allow by default:
|
||||
{"google.com", true},
|
||||
{"GOOGLE.com", true},
|
||||
|
||||
// Rejected by suffix:
|
||||
{"foo.TS.NET", false},
|
||||
{"foo.ts.net", false},
|
||||
|
||||
// Suffix doesn't match
|
||||
{"ts.net", true},
|
||||
|
||||
// Rejected by exact match:
|
||||
{"some.exact.bad", false},
|
||||
{"SOME.EXACT.BAD", false},
|
||||
|
||||
// But a prefix is okay.
|
||||
{"prefix-okay.some.exact.bad", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := b.allowExitNodeDNSProxyToServeName(tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("for %q = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
@@ -36,8 +36,10 @@ import (
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/empty"
|
||||
@@ -55,6 +57,7 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
@@ -63,7 +66,7 @@ import (
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
|
||||
func getControlDebugFlags() []string {
|
||||
if e := os.Getenv("TS_DEBUG_CONTROL_FLAGS"); e != "" {
|
||||
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
|
||||
return strings.Split(e, ",")
|
||||
}
|
||||
return nil
|
||||
@@ -88,6 +91,7 @@ type LocalBackend struct {
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store ipn.StateStore
|
||||
dialer *tsdial.Dialer // non-nil
|
||||
backendLogID string
|
||||
unregisterLinkMon func()
|
||||
unregisterHealthWatch func()
|
||||
@@ -97,9 +101,12 @@ type LocalBackend struct {
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
sshAtomicBool syncs.AtomicBool
|
||||
|
||||
filterHash deephash.Sum
|
||||
|
||||
filterAtomic atomic.Value // of *filter.Filter
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
@@ -139,7 +146,11 @@ type LocalBackend struct {
|
||||
// same as the Network Extension lifetime and we can thus avoid
|
||||
// double-copying files by writing them to the right location
|
||||
// immediately.
|
||||
directFileRoot string
|
||||
// It's also used on Synology & TrueNAS, but in that case DoFinalRename
|
||||
// is also set true, which moves the *.partial file to its final
|
||||
// name on completion.
|
||||
directFileRoot string
|
||||
directFileDoFinalRename bool // false on macOS, true on Synology & TrueNAS
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -153,16 +164,22 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
|
||||
|
||||
// NewLocalBackend returns a new LocalBackend that is ready to run,
|
||||
// but is not actually running.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
//
|
||||
// If dialer is nil, a new one is made.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine) (*LocalBackend, error) {
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
panic("ipn.NewLocalBackend: engine must not be nil")
|
||||
}
|
||||
|
||||
hi := hostinfo.New()
|
||||
logf("Host: %s/%s, %s", hi.OS, hi.GoArch, hi.OSVersion)
|
||||
envknob.LogCurrent(logf)
|
||||
if dialer == nil {
|
||||
dialer = new(tsdial.Dialer)
|
||||
}
|
||||
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
portpoll, err := portlist.NewPoller()
|
||||
if err != nil {
|
||||
@@ -177,11 +194,16 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
store: store,
|
||||
dialer: dialer,
|
||||
backendLogID: logid,
|
||||
state: ipn.NoState,
|
||||
portpoll: portpoll,
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||
|
||||
@@ -197,7 +219,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
wiredPeerAPIPort := false
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ig.GetInternals(); ok {
|
||||
tunWrap.PeerAPIPort = b.getPeerAPIPortForTSMPPing
|
||||
tunWrap.PeerAPIPort = b.GetPeerAPIPort
|
||||
wiredPeerAPIPort = true
|
||||
}
|
||||
}
|
||||
@@ -208,6 +230,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Dialer returns the backend's dialer.
|
||||
func (b *LocalBackend) Dialer() *tsdial.Dialer {
|
||||
return b.dialer
|
||||
}
|
||||
|
||||
// SetDirectFileRoot sets the directory to download files to directly,
|
||||
// without buffering them through an intermediate daemon-owned
|
||||
// tailcfg.UserID-specific directory.
|
||||
@@ -219,6 +246,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
|
||||
b.directFileRoot = dir
|
||||
}
|
||||
|
||||
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
|
||||
// a received "name.partial" file to "name" when the download is complete.
|
||||
//
|
||||
// This only applies when SetDirectFileRoot is non-empty.
|
||||
// The default is false.
|
||||
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.directFileDoFinalRename = v
|
||||
}
|
||||
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) maybePauseControlClientLocked() {
|
||||
if b.cc == nil {
|
||||
@@ -347,11 +385,23 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
if b.netMap != nil {
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
|
||||
s.TailnetName = b.netMap.Domain
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
if b.netMap != nil && b.netMap.SelfNode != nil {
|
||||
ss.ID = b.netMap.SelfNode.StableID
|
||||
ss.Online = health.GetInPollNetMap()
|
||||
if b.netMap != nil {
|
||||
ss.HostName = b.netMap.Hostinfo.Hostname
|
||||
ss.DNSName = b.netMap.Name
|
||||
ss.UserID = b.netMap.User
|
||||
if sn := b.netMap.SelfNode; sn != nil {
|
||||
ss.ID = sn.StableID
|
||||
if c := sn.Capabilities; len(c) > 0 {
|
||||
ss.Capabilities = append([]string(nil), c...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ss.HostName, _ = os.Hostname()
|
||||
}
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
|
||||
@@ -377,33 +427,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr4 string
|
||||
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
|
||||
for _, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
|
||||
if addr.IP().Is4() && tailAddr4 == "" {
|
||||
// The peer struct previously only allowed a single
|
||||
// Tailscale IP address. For compatibility for a few releases starting
|
||||
// with 1.8, keep it pulled out as IPv4-only for a bit.
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
exitNodeOption := tsaddr.PrefixesContainsFunc(p.AllowedIPs, func(r netaddr.IPPrefix) bool {
|
||||
return r.Bits() == 0
|
||||
})
|
||||
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailAddrDeprecated: tailAddr4,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
ExitNodeOption: exitNodeOption,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -499,6 +546,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// Since st.NetMap==nil means "netmap is unchanged", there is
|
||||
// no other way to represent this change.
|
||||
b.setNetMapLocked(nil)
|
||||
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||
}
|
||||
|
||||
prefs := b.prefs
|
||||
@@ -563,7 +611,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
b.logf("[v1] netmap diff: (none)")
|
||||
} else {
|
||||
b.logf("netmap diff:\n%v", diff)
|
||||
b.logf("[v1] netmap diff:\n%v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,6 +636,11 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
|
||||
// rather than by IP. It returns whether prefs was mutated.
|
||||
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
if nm == nil {
|
||||
// No netmap, can't resolve anything.
|
||||
return false
|
||||
}
|
||||
|
||||
// If we have a desired IP on file, try to find the corresponding
|
||||
// node.
|
||||
if b.prefs.ExitNodeIP.IsZero() {
|
||||
@@ -857,7 +910,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
timer := time.NewTimer(time.Second)
|
||||
select {
|
||||
case <-b.gotPortPollRes:
|
||||
b.logf("got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
|
||||
b.logf("[v1] got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
|
||||
timer.Stop()
|
||||
case <-timer.C:
|
||||
b.logf("timeout waiting for initial portlist")
|
||||
@@ -976,7 +1029,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
// wifi": you get internet access, but to additionally
|
||||
// get LAN access the LAN(s) need to be offered
|
||||
// explicitly as well.
|
||||
s, err := shrinkDefaultRoute(r)
|
||||
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
b.logf("getting local interface routes: %v", err)
|
||||
continue
|
||||
}
|
||||
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
|
||||
if err != nil {
|
||||
b.logf("computing default route filter: %v", err)
|
||||
continue
|
||||
@@ -1000,21 +1058,26 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
}
|
||||
|
||||
if !haveNetmap {
|
||||
b.logf("netmap packet filter: (not ready yet)")
|
||||
b.e.SetFilter(filter.NewAllowNone(b.logf, logNets))
|
||||
b.logf("[v1] netmap packet filter: (not ready yet)")
|
||||
b.setFilter(filter.NewAllowNone(b.logf, logNets))
|
||||
return
|
||||
}
|
||||
|
||||
oldFilter := b.e.GetFilter()
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
b.logf("[v1] netmap packet filter: (shields up)")
|
||||
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v filters", len(packetFilter))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
|
||||
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setFilter(f *filter.Filter) {
|
||||
b.filterAtomic.Store(f)
|
||||
b.e.SetFilter(f)
|
||||
}
|
||||
|
||||
var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
// RFC1918 LAN ranges
|
||||
netaddr.MustParseIPPrefix("192.168.0.0/16"),
|
||||
@@ -1115,17 +1178,14 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
}
|
||||
|
||||
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
|
||||
// minus those in removeFromDefaultRoute and local interface subnets.
|
||||
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
|
||||
interfaceRoutes, hostIPs, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// minus those in removeFromDefaultRoute and localInterfaceRoutes,
|
||||
// plus the IPs in hostIPs.
|
||||
func shrinkDefaultRoute(route netaddr.IPPrefix, localInterfaceRoutes *netaddr.IPSet, hostIPs []netaddr.IP) (*netaddr.IPSet, error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
// Add the default route.
|
||||
b.AddPrefix(route)
|
||||
// Remove the local interface routes.
|
||||
b.RemoveSet(interfaceRoutes)
|
||||
b.RemoveSet(localInterfaceRoutes)
|
||||
|
||||
// Having removed all the LAN subnets, re-add the hosts's own
|
||||
// IPs. It's fine for clients to connect to an exit node's public
|
||||
@@ -1242,7 +1302,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
|
||||
return
|
||||
}
|
||||
|
||||
if apiSrv != nil && apiSrv.hasFilesWaiting() {
|
||||
if apiSrv.hasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1357,7 @@ func (b *LocalBackend) popBrowserAuthNow() {
|
||||
}
|
||||
|
||||
// For testing lazy machine key generation.
|
||||
var panicOnMachineKeyGeneration, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_PANIC_MACHINE_KEY"))
|
||||
var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
|
||||
|
||||
func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
|
||||
var cache atomic.Value
|
||||
@@ -1447,19 +1507,19 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("using backend prefs")
|
||||
bs, err := b.store.ReadState(key)
|
||||
switch {
|
||||
case errors.Is(err, ipn.ErrStateNotExist):
|
||||
b.prefs = ipn.NewPrefs()
|
||||
b.prefs.WantRunning = false
|
||||
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
b.logf("using backend prefs; created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
return nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("store.ReadState(%q): %v", key, err)
|
||||
return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err)
|
||||
}
|
||||
b.prefs, err = ipn.PrefsFromBytes(bs, false)
|
||||
if err != nil {
|
||||
b.logf("using backend prefs for %q", key)
|
||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
}
|
||||
|
||||
@@ -1482,7 +1542,10 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
b.logf("using backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
|
||||
b.sshAtomicBool.Set(b.prefs != nil && b.prefs.RunSSH)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1651,14 +1714,20 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done.
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
|
||||
b.sshAtomicBool.Set(newp.RunSSH)
|
||||
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
b.prefs = newp
|
||||
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
b.findExitNodeIDLocked(netMap)
|
||||
b.inServerMode = newp.ForceDaemon
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
newp = b.prefs.Clone()
|
||||
@@ -1694,7 +1763,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
// notified (to update its prefs/persist) on
|
||||
// account switch. Log this while we figure it
|
||||
// out.
|
||||
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
|
||||
b.logf("active login: %q ([unexpected] corp#461, not %q)", newp.Persist.LoginName, login)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1723,7 +1792,9 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok bool) {
|
||||
// GetPeerAPIPort returns the port number for the peerapi server
|
||||
// running on the provided IP.
|
||||
func (b *LocalBackend) GetPeerAPIPort(ip netaddr.IP) (port uint16, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
@@ -1734,17 +1805,47 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ServePeerAPIConnection serves an already-accepted connection c.
|
||||
//
|
||||
// The remote parameter is the remote address.
|
||||
// The local paramater is the local address (either a Tailscale IPv4
|
||||
// or IPv6 IP and the peerapi port for that address).
|
||||
//
|
||||
// The connection will be closed by ServePeerAPIConnection.
|
||||
func (b *LocalBackend) ServePeerAPIConnection(remote, local netaddr.IPPort, c net.Conn) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
if pln.ip == local.IP() {
|
||||
go pln.ServeConn(remote, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
b.logf("[unexpected] no peerAPI listener found for %v", local)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.ServiceProto("peerapi4")
|
||||
proto := tailcfg.PeerAPI4
|
||||
if pln.ip.Is6() {
|
||||
proto = "peerapi6"
|
||||
proto = tailcfg.PeerAPI6
|
||||
}
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: proto,
|
||||
Port: uint16(pln.port),
|
||||
})
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
|
||||
// These are the platforms currently supported by
|
||||
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: tailcfg.PeerAPIDNS,
|
||||
Port: 1, // version
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1819,15 +1920,15 @@ func (b *LocalBackend) authReconfig() {
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
b.logf("authReconfig: blocked, skipping.")
|
||||
b.logf("[v1] authReconfig: blocked, skipping.")
|
||||
return
|
||||
}
|
||||
if nm == nil {
|
||||
b.logf("authReconfig: netmap not yet valid. Skipping.")
|
||||
b.logf("[v1] authReconfig: netmap not yet valid. Skipping.")
|
||||
return
|
||||
}
|
||||
if !prefs.WantRunning {
|
||||
b.logf("authReconfig: skipping because !WantRunning.")
|
||||
b.logf("[v1] authReconfig: skipping because !WantRunning.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1845,6 +1946,15 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the dialer updated about whether we're supposed to use
|
||||
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
|
||||
// can use it for name resolution)
|
||||
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
|
||||
b.dialer.SetExitDNSDoH(dohURL)
|
||||
} else {
|
||||
b.dialer.SetExitDNSDoH("")
|
||||
}
|
||||
|
||||
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
|
||||
if err != nil {
|
||||
b.logf("wgcfg: %v", err)
|
||||
@@ -1877,6 +1987,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
// selfV6Only is whether we only have IPv6 addresses ourselves.
|
||||
selfV6Only := tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
|
||||
!tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
|
||||
dcfg.OnlyIPv6 = selfV6Only
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
// quad-100 can always respond to MagicDNS queries, even if the OS
|
||||
@@ -1943,12 +2054,32 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
return dcfg
|
||||
}
|
||||
|
||||
for _, dom := range nm.DNS.Domains {
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
logf("[unexpected] non-FQDN search domain %q", dom)
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
addDefault := func(resolvers []dnstype.Resolver) {
|
||||
for _, r := range resolvers {
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
|
||||
}
|
||||
}
|
||||
|
||||
// If we're using an exit node and that exit node is new enough (1.19.x+)
|
||||
// to run a DoH DNS proxy, then send all our DNS traffic through it.
|
||||
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
|
||||
addDefault([]dnstype.Resolver{{Addr: dohURL}})
|
||||
return dcfg
|
||||
}
|
||||
|
||||
addDefault(nm.DNS.Resolvers)
|
||||
for suffix, resolvers := range nm.DNS.Routes {
|
||||
fqdn, err := dnsname.ToFQDN(suffix)
|
||||
@@ -1970,18 +2101,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
|
||||
}
|
||||
}
|
||||
for _, dom := range nm.DNS.Domains {
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
logf("[unexpected] non-FQDN search domain %q", dom)
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
// Set FallbackResolvers as the default resolvers in the
|
||||
// scenarios that can't handle a purely split-DNS config. See
|
||||
@@ -2005,9 +2124,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
case len(dcfg.Routes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case versionOS == "android":
|
||||
// We don't support split DNS at all on Android yet.
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
}
|
||||
|
||||
return dcfg
|
||||
@@ -2041,7 +2157,7 @@ func (b *LocalBackend) TailscaleVarRoot() string {
|
||||
return b.varRoot
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
case "ios", "android", "darwin":
|
||||
dir, _ := paths.AppSharedDir.Load().(string)
|
||||
return dir
|
||||
}
|
||||
@@ -2054,7 +2170,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
}
|
||||
varRoot := b.TailscaleVarRoot()
|
||||
if varRoot == "" {
|
||||
b.logf("peerapi disabled; no state directory")
|
||||
b.logf("Taildrop disabled; no state directory")
|
||||
return ""
|
||||
}
|
||||
baseDir := fmt.Sprintf("%s-uid-%d",
|
||||
@@ -2062,7 +2178,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
uid)
|
||||
dir := filepath.Join(varRoot, "files", baseDir)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
b.logf("peerapi disabled; error making directory: %v", err)
|
||||
b.logf("Taildrop disabled; error making directory: %v", err)
|
||||
return ""
|
||||
}
|
||||
return dir
|
||||
@@ -2125,22 +2241,20 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
fileRoot := b.fileRootLocked(selfNode.User)
|
||||
if fileRoot == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var tunName string
|
||||
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ge.GetInternals(); ok {
|
||||
tunName, _ = tunWrap.Name()
|
||||
}
|
||||
b.logf("peerapi starting without Taildrop directory configured")
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
directFileDoFinalRename: b.directFileDoFinalRename,
|
||||
}
|
||||
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
|
||||
if r, ok := re.GetResolver(); ok {
|
||||
ps.resolver = r
|
||||
}
|
||||
}
|
||||
b.peerAPIServer = ps
|
||||
|
||||
@@ -2305,7 +2419,9 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
}
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
if tsaddr.PrefixesContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
@@ -2537,8 +2653,11 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.activeLogin = ""
|
||||
b.sshAtomicBool.Set(false)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Get() }
|
||||
|
||||
// Logout tells the controlclient that we want to log out, and
|
||||
// transitions the local engine to the logged-out state without
|
||||
// waiting for controlclient to be in that state.
|
||||
@@ -2618,6 +2737,7 @@ func hasCapability(nm *netmap.NetworkMap, cap string) bool {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
b.dialer.SetNetMap(nm)
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = nm.UserProfiles[nm.User].LoginName
|
||||
@@ -2722,9 +2842,6 @@ func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.WaitingFiles()
|
||||
}
|
||||
|
||||
@@ -2732,9 +2849,6 @@ func (b *LocalBackend) DeleteFile(name string) error {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.DeleteFile(name)
|
||||
}
|
||||
|
||||
@@ -2742,9 +2856,6 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, 0, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.OpenFile(name)
|
||||
}
|
||||
|
||||
@@ -2858,9 +2969,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
var p4, p6 uint16
|
||||
for _, s := range peer.Hostinfo.Services {
|
||||
switch s.Proto {
|
||||
case "peerapi4":
|
||||
case tailcfg.PeerAPI4:
|
||||
p4 = s.Port
|
||||
case "peerapi6":
|
||||
case tailcfg.PeerAPI6:
|
||||
p6 = s.Port
|
||||
}
|
||||
}
|
||||
@@ -2894,48 +3005,97 @@ func (b *LocalBackend) CheckIPForwarding() error {
|
||||
if wgengine.IsNetstackRouter(b.e) {
|
||||
return nil
|
||||
}
|
||||
if isBSD(runtime.GOOS) {
|
||||
|
||||
switch {
|
||||
case isBSD(runtime.GOOS):
|
||||
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
|
||||
case runtime.GOOS == "linux":
|
||||
return checkIPForwardingLinux()
|
||||
default:
|
||||
// TODO: subnet routing and exit nodes probably don't work
|
||||
// correctly on non-linux, non-netstack OSes either. Warn
|
||||
// instead of being silent?
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// checkIPForwardingLinux checks if IP forwarding is enabled correctly
|
||||
// for subnet routing and exit node functionality. Returns an error
|
||||
// describing configuration issues if the configuration is not
|
||||
// definitely good.
|
||||
func checkIPForwardingLinux() error {
|
||||
const kbLink = "\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
|
||||
|
||||
disabled, err := disabledSysctls("net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
||||
}
|
||||
|
||||
var keys []string
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
keys = append(keys, "net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
|
||||
} else if isBSD(runtime.GOOS) {
|
||||
keys = append(keys, "net.inet.ip.forwarding")
|
||||
} else {
|
||||
if len(disabled) == 0 {
|
||||
// IP forwarding is enabled systemwide, all is well.
|
||||
return nil
|
||||
}
|
||||
|
||||
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
|
||||
for _, key := range keys {
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
|
||||
// IP forwarding isn't enabled globally, but it might be enabled
|
||||
// on a per-interface basis. Check if it's on for all interfaces,
|
||||
// and warn appropriately if it's not.
|
||||
ifaces, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't enumerate network interfaces, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
||||
}
|
||||
|
||||
var (
|
||||
warnings []string
|
||||
anyEnabled bool
|
||||
)
|
||||
for _, iface := range ifaces {
|
||||
if iface.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
disabled, err = disabledSysctls(fmt.Sprintf("net.ipv4.conf.%s.forwarding", iface.Name), fmt.Sprintf("net.ipv6.conf.%s.forwarding", iface.Name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix)
|
||||
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
|
||||
}
|
||||
if !on {
|
||||
return fmt.Errorf("%s is disabled.%s", key, suffix)
|
||||
if len(disabled) > 0 {
|
||||
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, strings.Join(disabled, ", ")))
|
||||
} else {
|
||||
anyEnabled = true
|
||||
}
|
||||
}
|
||||
if !anyEnabled {
|
||||
// IP forwarding is compeltely disabled, just say that rather
|
||||
// than enumerate all the interfaces on the system.
|
||||
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink)
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
// If partially enabled, enumerate the bits that won't work.
|
||||
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// peerDialControlFunc is non-nil on platforms that require a way to
|
||||
// bind to dial out to other peers.
|
||||
var peerDialControlFunc func(*LocalBackend) func(network, address string, c syscall.RawConn) error
|
||||
|
||||
// PeerDialControlFunc returns a net.Dialer.Control func (possibly nil) to use to
|
||||
// dial other Tailscale peers from the current environment.
|
||||
func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
|
||||
if peerDialControlFunc != nil {
|
||||
return peerDialControlFunc(b)
|
||||
// disabledSysctls checks if the given sysctl keys are off, according
|
||||
// to strconv.ParseBool. Returns a list of keys that are disabled, or
|
||||
// err if something went wrong which prevented the lookups from
|
||||
// completing.
|
||||
func disabledSysctls(sysctls ...string) (disabled []string, err error) {
|
||||
for _, k := range sysctls {
|
||||
// TODO: on linux, we can get at these values via /proc/sys,
|
||||
// rather than fork subcommands that may not be installed.
|
||||
bs, err := exec.Command("sysctl", "-n", k).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't check %s (%v)", k, err)
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse %s (%v)", k, err)
|
||||
}
|
||||
if !on {
|
||||
disabled = append(disabled, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return disabled, nil
|
||||
}
|
||||
|
||||
// DERPMap returns the current DERPMap in use, or nil if not connected.
|
||||
@@ -2947,3 +3107,107 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
|
||||
}
|
||||
return b.netMap.DERPMap
|
||||
}
|
||||
|
||||
// OfferingExitNode reports whether b is currently offering exit node
|
||||
// access.
|
||||
func (b *LocalBackend) OfferingExitNode() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.prefs == nil {
|
||||
return false
|
||||
}
|
||||
var def4, def6 bool
|
||||
for _, r := range b.prefs.AdvertiseRoutes {
|
||||
if r.Bits() != 0 {
|
||||
continue
|
||||
}
|
||||
if r.IP().Is4() {
|
||||
def4 = true
|
||||
} else if r.IP().Is6() {
|
||||
def6 = true
|
||||
}
|
||||
}
|
||||
return def4 && def6
|
||||
}
|
||||
|
||||
// allowExitNodeDNSProxyToServeName reports whether the Exit Node DNS
|
||||
// proxy is allowed to serve responses for the provided DNS name.
|
||||
func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return false
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
for _, bad := range nm.DNS.ExitNodeFilteredSet {
|
||||
if bad == "" {
|
||||
// Invalid, ignore.
|
||||
continue
|
||||
}
|
||||
if bad[0] == '.' {
|
||||
// Entries beginning with a dot are suffix matches.
|
||||
if dnsname.HasSuffix(name, bad) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Otherwise entries are exact matches. They're
|
||||
// guaranteed to be lowercase already.
|
||||
if name == bad {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
|
||||
// to exitNodeID's DoH service, if available.
|
||||
//
|
||||
// If exitNodeID is the zero valid, it returns "", false.
|
||||
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
|
||||
if exitNodeID.IsZero() {
|
||||
return "", false
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.StableID != exitNodeID {
|
||||
continue
|
||||
}
|
||||
for _, s := range p.Hostinfo.Services {
|
||||
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
return peerAPIBase(nm, p) + "/dns-query", true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugRebind() error {
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc.Rebind()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugReSTUN() error {
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc.ReSTUN("explicit-debug")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||
ig, ok := b.e.(wgengine.InternalsGetter)
|
||||
if !ok {
|
||||
return nil, errors.New("engine isn't InternalsGetter")
|
||||
}
|
||||
_, mc, ok := ig.GetInternals()
|
||||
if !ok {
|
||||
return nil, errors.New("failed to get internals")
|
||||
}
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
@@ -117,8 +117,8 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -178,9 +178,31 @@ func TestShrinkDefaultRoute(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Construct a fake local network environment to make this test hermetic.
|
||||
// localInterfaceRoutes and hostIPs would normally come from calling interfaceRoutes,
|
||||
// and localAddresses would normally come from calling interfaces.LocalAddresses.
|
||||
var b netaddr.IPSetBuilder
|
||||
for _, c := range []string{"127.0.0.0/8", "192.168.9.0/24", "fe80::/32"} {
|
||||
p := netaddr.MustParseIPPrefix(c)
|
||||
b.AddPrefix(p)
|
||||
}
|
||||
localInterfaceRoutes, err := b.IPSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostIPs := []netaddr.IP{
|
||||
netaddr.MustParseIP("127.0.0.1"),
|
||||
netaddr.MustParseIP("192.168.9.39"),
|
||||
netaddr.MustParseIP("fe80::1"),
|
||||
netaddr.MustParseIP("fe80::437d:feff:feca:49a7"),
|
||||
}
|
||||
localAddresses := []netaddr.IP{
|
||||
netaddr.MustParseIP("192.168.9.39"),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
def := netaddr.MustParseIPPrefix(test.route)
|
||||
got, err := shrinkDefaultRoute(def)
|
||||
got, err := shrinkDefaultRoute(def, localInterfaceRoutes, hostIPs)
|
||||
if err != nil {
|
||||
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
|
||||
}
|
||||
@@ -194,11 +216,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
|
||||
}
|
||||
}
|
||||
ips, _, err := interfaces.LocalAddresses()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
for _, ip := range localAddresses {
|
||||
want := test.localIPFn(ip)
|
||||
if gotContains := got.Contains(ip); gotContains != want {
|
||||
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)
|
||||
@@ -445,7 +463,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(eng.Close)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, eng)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, nil, eng)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, e)
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -28,34 +29,50 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
|
||||
|
||||
// addH2C is non-nil on platforms where we want to add H2C
|
||||
// ("cleartext" HTTP/2) support to the peerAPI.
|
||||
var addH2C func(*http.Server)
|
||||
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
rootDir string // empty means file receiving unavailable
|
||||
selfNode *tailcfg.Node
|
||||
knownEmpty syncs.AtomicBool
|
||||
resolver *resolver.Resolver
|
||||
|
||||
// directFileMode is whether we're writing files directly to a
|
||||
// download directory (as *.partial files), rather than making
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on GUI macOS version.
|
||||
// somewhere itself. This is used on the GUI macOS versions
|
||||
// and on Synology.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
// from "foo.jpg.partial" to "foo.jpg" unless
|
||||
// directFileDoFinalRename is set.
|
||||
directFileMode bool
|
||||
|
||||
// directFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
directFileDoFinalRename bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -72,6 +89,10 @@ const (
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func (s *peerAPIServer) canReceiveFiles() bool {
|
||||
return s != nil && s.rootDir != ""
|
||||
}
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
@@ -118,7 +139,7 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
if s.rootDir == "" || s.directFileMode {
|
||||
if s == nil || s.rootDir == "" || s.directFileMode {
|
||||
return false
|
||||
}
|
||||
if s.knownEmpty.Get() {
|
||||
@@ -178,8 +199,11 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
// As a side effect, it also does any lazy deletion of files as
|
||||
// required by Windows.
|
||||
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s == nil {
|
||||
return nil, errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, nil
|
||||
@@ -243,6 +267,11 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
|
||||
errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
|
||||
)
|
||||
|
||||
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
|
||||
// it failed earlier. This happens on Windows when various anti-virus
|
||||
// tools hook into filesystem operations and have the file open still
|
||||
@@ -258,8 +287,11 @@ func tryDeleteAgain(fullPath string) {
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s == nil {
|
||||
return errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
return errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return errors.New("deletes not allowed in direct mode")
|
||||
@@ -324,8 +356,11 @@ func touchFile(path string) error {
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if s == nil {
|
||||
return nil, 0, errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return nil, 0, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, 0, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, 0, errors.New("opens not allowed in direct mode")
|
||||
@@ -358,7 +393,7 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
// On iOS/macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to, to get
|
||||
// out of the network sandbox.
|
||||
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
|
||||
if err := initListenConfig(&lc, ip, ifState, s.b.dialer.TUNName()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
@@ -447,44 +482,34 @@ func (pln *peerAPIListener) serve() {
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", ipp)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
pln.ServeConn(ipp, c)
|
||||
}
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
net.Listener
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
func (pln *peerAPIListener) ServeConn(src netaddr.IPPort, c net.Conn) {
|
||||
logf := pln.lb.logf
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(src)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", src)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: src,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(netutil.NewOneConnListenerFrom(c, pln.ln))
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
// peerAPIHandler serves the Peer API for a source specific client.
|
||||
type peerAPIHandler struct {
|
||||
ps *peerAPIServer
|
||||
@@ -503,6 +528,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/dns-query") {
|
||||
h.handleDNSQuery(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/v0/goroutines":
|
||||
h.handleServeGoroutines(w, r)
|
||||
@@ -513,6 +542,12 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/metrics":
|
||||
h.handleServeMetrics(w, r)
|
||||
return
|
||||
case "/v0/magicsock":
|
||||
h.handleServeMagicsock(w, r)
|
||||
return
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
@@ -599,7 +634,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
@@ -671,7 +706,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
@@ -741,6 +776,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
eng := h.ps.b.e
|
||||
if ig, ok := eng.(wgengine.InternalsGetter); ok {
|
||||
if _, mc, ok := ig.GetInternals(); ok {
|
||||
mc.ServeHTTPDebug(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, "miswired", 500)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
@@ -749,3 +799,231 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dh := health.DebugHandler("dnsfwd")
|
||||
if dh == nil {
|
||||
http.Error(w, "not wired up", 500)
|
||||
return
|
||||
}
|
||||
dh.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) replyToDNSQueries() bool {
|
||||
if h.isSelf {
|
||||
// If the peer is owned by the same user, just allow it
|
||||
// without further checks.
|
||||
return true
|
||||
}
|
||||
b := h.ps.b
|
||||
if !b.OfferingExitNode() {
|
||||
// If we're not an exit node, there's no point to
|
||||
// being a DNS server for somebody.
|
||||
return false
|
||||
}
|
||||
if !h.remoteAddr.IsValid() {
|
||||
// This should never be the case if the peerAPIHandler
|
||||
// was wired up correctly, but just in case.
|
||||
return false
|
||||
}
|
||||
// Otherwise, we're an exit node but the peer is not us, so
|
||||
// we need to check if they're allowed access to the internet.
|
||||
// As peerapi bypasses wgengine/filter checks, we need to check
|
||||
// ourselves. As a proxy for autogroup:internet access, we see
|
||||
// if we would've accepted a packet to 0.0.0.0:53. We treat
|
||||
// the IP 0.0.0.0 as being "the internet".
|
||||
f, ok := b.filterAtomic.Load().(*filter.Filter)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Note: we check TCP here because the Filter type already had
|
||||
// a CheckTCP method (for unit tests), but it's pretty
|
||||
// arbitrary. DNS runs over TCP and UDP, so sure... we check
|
||||
// TCP.
|
||||
dstIP := netaddr.IPv4(0, 0, 0, 0)
|
||||
remoteIP := h.remoteAddr.IP()
|
||||
if remoteIP.Is6() {
|
||||
// autogroup:internet for IPv6 is defined to start with 2000::/3,
|
||||
// so use 2000::0 as the probe "the internet" address.
|
||||
dstIP = netaddr.MustParseIP("2000::")
|
||||
}
|
||||
verdict := f.CheckTCP(remoteIP, dstIP, 53)
|
||||
return verdict == filter.Accept
|
||||
}
|
||||
|
||||
// handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
|
||||
// It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
|
||||
func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ps.resolver == nil {
|
||||
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
http.Error(w, "DNS access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
pretty := false // non-DoH debug mode for humans
|
||||
q, publicError := dohQuery(r)
|
||||
if publicError != "" && r.Method == "GET" {
|
||||
if name := r.FormValue("q"); name != "" {
|
||||
pretty = true
|
||||
publicError = ""
|
||||
q = dnsQueryForName(name, r.FormValue("t"))
|
||||
}
|
||||
}
|
||||
if publicError != "" {
|
||||
http.Error(w, publicError, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Some timeout that's short enough to be noticed by humans
|
||||
// but long enough that it's longer than real DNS timeouts.
|
||||
const arbitraryTimeout = 5 * time.Second
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
|
||||
defer cancel()
|
||||
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr, h.ps.b.allowExitNodeDNSProxyToServeName)
|
||||
if err != nil {
|
||||
h.logf("handleDNS fwd error: %v", err)
|
||||
if err := ctx.Err(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
} else {
|
||||
http.Error(w, "DNS forwarding error", 500)
|
||||
}
|
||||
return
|
||||
}
|
||||
if pretty {
|
||||
// Non-standard response for interactive debugging.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
writePrettyDNSReply(w, res)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/dns-message")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(res)))
|
||||
w.Write(res)
|
||||
}
|
||||
|
||||
func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
|
||||
const maxQueryLen = 256 << 10
|
||||
switch r.Method {
|
||||
default:
|
||||
return nil, "bad HTTP method"
|
||||
case "GET":
|
||||
q64 := r.FormValue("dns")
|
||||
if q64 == "" {
|
||||
return nil, "missing 'dns' parameter"
|
||||
}
|
||||
if base64.RawURLEncoding.DecodedLen(len(q64)) > maxQueryLen {
|
||||
return nil, "query too large"
|
||||
}
|
||||
q, err := base64.RawURLEncoding.DecodeString(q64)
|
||||
if err != nil {
|
||||
return nil, "invalid 'dns' base64 encoding"
|
||||
}
|
||||
return q, ""
|
||||
case "POST":
|
||||
if r.Header.Get("Content-Type") != "application/dns-message" {
|
||||
return nil, "unexpected Content-Type"
|
||||
}
|
||||
q, err := io.ReadAll(io.LimitReader(r.Body, maxQueryLen+1))
|
||||
if err != nil {
|
||||
return nil, "error reading post body with DNS query"
|
||||
}
|
||||
if len(q) > maxQueryLen {
|
||||
return nil, "query too large"
|
||||
}
|
||||
return q, ""
|
||||
}
|
||||
}
|
||||
|
||||
func dnsQueryForName(name, typStr string) []byte {
|
||||
typ := dnsmessage.TypeA
|
||||
switch strings.ToLower(typStr) {
|
||||
case "aaaa":
|
||||
typ = dnsmessage.TypeAAAA
|
||||
case "txt":
|
||||
typ = dnsmessage.TypeTXT
|
||||
}
|
||||
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
|
||||
OpCode: 0, // query
|
||||
RecursionDesired: true,
|
||||
ID: 0,
|
||||
})
|
||||
if !strings.HasSuffix(name, ".") {
|
||||
name += "."
|
||||
}
|
||||
b.StartQuestions()
|
||||
b.Question(dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName(name),
|
||||
Type: typ,
|
||||
Class: dnsmessage.ClassINET,
|
||||
})
|
||||
msg, _ := b.Finish()
|
||||
return msg
|
||||
}
|
||||
|
||||
func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
j, _ := json.Marshal(struct {
|
||||
Error string
|
||||
}{err.Error()})
|
||||
j = append(j, '\n')
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
}()
|
||||
var p dnsmessage.Parser
|
||||
hdr, err := p.Start(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hdr.RCode != dnsmessage.RCodeSuccess {
|
||||
return fmt.Errorf("DNS RCode = %v", hdr.RCode)
|
||||
}
|
||||
if err := p.SkipAllQuestions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gotIPs []string
|
||||
for {
|
||||
h, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if h.Class != dnsmessage.ClassINET {
|
||||
continue
|
||||
}
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeA:
|
||||
r, err := p.AResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotIPs = append(gotIPs, net.IP(r.A[:]).String())
|
||||
case dnsmessage.TypeAAAA:
|
||||
r, err := p.AAAAResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotIPs = append(gotIPs, net.IP(r.AAAA[:]).String())
|
||||
case dnsmessage.TypeTXT:
|
||||
r, err := p.TXTResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotIPs = append(gotIPs, r.TXT...)
|
||||
}
|
||||
}
|
||||
j, _ := json.Marshal(gotIPs)
|
||||
j = append(j, '\n')
|
||||
w.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func init() {
|
||||
addH2C = func(s *http.Server) {
|
||||
h2s := &http2.Server{}
|
||||
s.Handler = h2c.NewHandler(s.Handler, h2s)
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,8 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -21,7 +19,6 @@ import (
|
||||
|
||||
func init() {
|
||||
initListenConfig = initListenConfigNetworkExtension
|
||||
peerDialControlFunc = peerDialControlFuncNetworkExtension
|
||||
}
|
||||
|
||||
// initListenConfigNetworkExtension configures nc for listening on IP
|
||||
@@ -34,24 +31,3 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
|
||||
}
|
||||
return netns.SetListenConfigInterfaceIndex(nc, tunIf.Index)
|
||||
}
|
||||
|
||||
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
st := b.prevIfState
|
||||
pas := b.peerAPIServer
|
||||
index := -1
|
||||
if st != nil && pas != nil && pas.tunName != "" {
|
||||
if tunIf, ok := st.Interface[pas.tunName]; ok {
|
||||
index = tunIf.Index
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
netns.SetListenConfigInterfaceIndex(&lc, index)
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
if index == -1 {
|
||||
return errors.New("failed to find TUN interface to bind to")
|
||||
}
|
||||
return lc.Control(network, address, c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type peerAPITestEnv struct {
|
||||
@@ -174,7 +179,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
bodyContains("no rootdir"),
|
||||
bodyContains("Taildrop disabled; no storage directory"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -568,3 +573,55 @@ func TestDeletedMarkers(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
|
||||
h.isSelf = true
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("for isSelf = false; want true")
|
||||
}
|
||||
h.isSelf = false
|
||||
h.remoteAddr = netaddr.MustParseIPPort("100.150.151.152:12345")
|
||||
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
},
|
||||
}
|
||||
if h.ps.b.OfferingExitNode() {
|
||||
t.Fatal("unexpectedly offering exit node")
|
||||
}
|
||||
h.ps.b.prefs = &ipn.Prefs{
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
}
|
||||
if !h.ps.b.OfferingExitNode() {
|
||||
t.Fatal("unexpectedly not offering exit node")
|
||||
}
|
||||
|
||||
if h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly doing DNS without filter")
|
||||
}
|
||||
|
||||
h.ps.b.setFilter(filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)))
|
||||
if h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly doing DNS without filter")
|
||||
}
|
||||
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
|
||||
h.ps.b.setFilter(f)
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
// Also test IPv6.
|
||||
h.remoteAddr = netaddr.MustParseIPPort("[fe70::1]:12345")
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@ func (nt *notifyThrottler) drain(count int) []ipn.Notify {
|
||||
type mockControl struct {
|
||||
tb testing.TB
|
||||
opts controlclient.Options
|
||||
logf logger.Logf
|
||||
logfActual logger.Logf
|
||||
statusFunc func(controlclient.Status)
|
||||
preventLog syncs.AtomicBool
|
||||
|
||||
mu sync.Mutex
|
||||
calls []string
|
||||
@@ -104,6 +105,13 @@ func newMockControl(tb testing.TB) *mockControl {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *mockControl) logf(format string, args ...interface{}) {
|
||||
if cc.preventLog.Get() || cc.logfActual == nil {
|
||||
return
|
||||
}
|
||||
cc.logfActual(format, args...)
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
|
||||
cc.statusFunc = fn
|
||||
}
|
||||
@@ -284,14 +292,15 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
cc := newMockControl(t)
|
||||
b, err := NewLocalBackend(logf, "logid", store, e)
|
||||
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
cc.opts = opts
|
||||
cc.logf = opts.Logf
|
||||
cc.logfActual = opts.Logf
|
||||
cc.authBlocked = true
|
||||
cc.persist = cc.opts.Persist
|
||||
cc.mu.Unlock()
|
||||
@@ -305,6 +314,9 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(0)
|
||||
|
||||
b.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if cc.preventLog.Get() {
|
||||
return
|
||||
}
|
||||
if n.State != nil ||
|
||||
n.Prefs != nil ||
|
||||
n.BrowseToURL != nil ||
|
||||
@@ -315,6 +327,7 @@ func TestStateMachine(t *testing.T) {
|
||||
logf("\n(ignored) %v\n\n", n)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { b.SetNotifyCallback(nil) }) // hacky way to pacify issue 3020
|
||||
|
||||
// Check that it hasn't called us right away.
|
||||
// The state machine should be idle until we call Start().
|
||||
@@ -941,14 +954,14 @@ func TestWGEngineStatusRace(t *testing.T) {
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(eng.Close)
|
||||
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), eng)
|
||||
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cc := newMockControl(t)
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.logf = opts.Logf
|
||||
cc.logfActual = opts.Logf
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
|
||||
74
ipn/ipnserver/proxyconnect.go
Normal file
74
ipn/ipnserver/proxyconnect.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// handleProxyConnectConn handles a CONNECT request to
|
||||
// log.tailscale.io (or whatever the configured log server is). This
|
||||
// is intended for use by the Windows GUI client to log via when an
|
||||
// exit node is in use, so the logs don't go out via the exit node and
|
||||
// instead go directly, like tailscaled's. The dialer tried to do that
|
||||
// in the unprivileged GUI by binding to a specific interface, but the
|
||||
// "Internet Kill Switch" installed by tailscaled for exit nodes
|
||||
// precludes that from working and instead the GUI fails to dial out.
|
||||
// So, go through tailscaled (with a CONNECT request) instead.
|
||||
func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) {
|
||||
defer c.Close()
|
||||
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
logf("ReadRequest: %v", err)
|
||||
return
|
||||
}
|
||||
c.SetReadDeadline(time.Time{})
|
||||
|
||||
if req.Method != "CONNECT" {
|
||||
logf("ReadRequest: unexpected method %q, not CONNECT", req.Method)
|
||||
return
|
||||
}
|
||||
|
||||
hostPort := req.RequestURI
|
||||
logHost := logpolicy.LogHost()
|
||||
allowed := net.JoinHostPort(logHost, "443")
|
||||
if hostPort != allowed {
|
||||
logf("invalid CONNECT target %q; want %q", hostPort, allowed)
|
||||
io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n")
|
||||
return
|
||||
}
|
||||
|
||||
tr := logpolicy.NewLogtailTransport(logHost)
|
||||
back, err := tr.DialContext(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
logf("error CONNECT dialing %v: %v", hostPort, err)
|
||||
io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n")
|
||||
return
|
||||
}
|
||||
defer back.Close()
|
||||
|
||||
io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n")
|
||||
|
||||
errc := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(c, back)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(back, br)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,13 +32,15 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"inet.af/peercred"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/ipn/store/aws"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
@@ -45,9 +48,11 @@ import (
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
@@ -179,6 +184,13 @@ func (s *Server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
func lookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||
// The below workaround is only applicable when uid represents a
|
||||
// valid security principal. Omitting this check causes us to succeed
|
||||
// even when uid represents a deleted user.
|
||||
if !winutil.IsSIDValidPrincipal(uid) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
@@ -237,12 +249,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
||||
}
|
||||
|
||||
// bufferIsConnect reports whether br looks like it's likely an HTTP
|
||||
// CONNECT request.
|
||||
//
|
||||
// Invariant: br has already had at least 4 bytes Peek'ed.
|
||||
func bufferIsConnect(br *bufio.Reader) bool {
|
||||
peek, _ := br.Peek(br.Buffered())
|
||||
return mem.HasPrefix(mem.B(peek), mem.S("CONN"))
|
||||
}
|
||||
|
||||
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
|
||||
// Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn)
|
||||
if bufferIsConnect(br) {
|
||||
s.handleProxyConnectConn(ctx, br, c, logf)
|
||||
return
|
||||
}
|
||||
|
||||
isHTTPReq := bufferHasHTTPRequest(br)
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
@@ -281,7 +309,7 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
ErrorLog: logger.StdLogger(logf),
|
||||
Handler: s.localhostHandler(ci),
|
||||
}
|
||||
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
|
||||
httpServer.Serve(netutil.NewOneConnListener(&protoSwitchConn{s: s, br: br, Conn: c}))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -427,6 +455,26 @@ func (s *Server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// connCanFetchCerts reports whether ci is allowed to fetch HTTPS
|
||||
// certs from this server when it wouldn't otherwise be able to.
|
||||
//
|
||||
// That is, this reports whether ci should grant additional
|
||||
// capabilities over what the conn would otherwise be able to do.
|
||||
//
|
||||
// For now this only returns true on Unix machines when
|
||||
// TS_PERMIT_CERT_UID is set the to the userid of the peer
|
||||
// connection. It's intended to give your non-root webserver access
|
||||
// (www-data, caddy, nginx, etc) to certs.
|
||||
func (s *Server) connCanFetchCerts(ci connIdentity) bool {
|
||||
if ci.IsUnixSock && ci.Creds != nil {
|
||||
connUID, ok := ci.Creds.UserID()
|
||||
if ok && connUID == envknob.String("TS_PERMIT_CERT_UID") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *Server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
@@ -651,7 +699,7 @@ func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
//
|
||||
// Deprecated: use New and Server.Run instead.
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
@@ -735,7 +783,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
}
|
||||
}
|
||||
|
||||
server, err := New(logf, logid, store, eng, serverModeUser, opts)
|
||||
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -748,8 +796,8 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
// New returns a new Server.
|
||||
//
|
||||
// To start it, use the Server.Run method.
|
||||
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -758,6 +806,22 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
|
||||
dg := distro.Get()
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
if err != nil {
|
||||
logf("%s Taildrop support: %v", dg, err)
|
||||
} else {
|
||||
logf("%s Taildrop: using %v", dg, path)
|
||||
b.SetDirectFileRoot(path)
|
||||
b.SetDirectFileDoFinalRename(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
@@ -851,14 +915,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(args) != 2 && args[0] != "/subproc" {
|
||||
panic(fmt.Sprintf("unexpected arguments %q", args))
|
||||
}
|
||||
logID := args[1]
|
||||
logf = filelogger.New("tailscale-service", logID, logf)
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
@@ -890,6 +946,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
if runtime.GOOS == "windows" {
|
||||
extraEnv, err := loadExtraEnv()
|
||||
if err != nil {
|
||||
logf("errors loading extra env file; ignoring: %v", err)
|
||||
} else {
|
||||
cmd.Env = append(os.Environ(), extraEnv...)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
// When the writer goes away, the reader gets EOF.
|
||||
@@ -998,29 +1062,6 @@ func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) fun
|
||||
}
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
|
||||
// it's already had a few bytes read from it to determine that it's
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
@@ -1041,6 +1082,7 @@ func (psc *protoSwitchConn) Close() error {
|
||||
func (s *Server) localhostHandler(ci connIdentity) http.Handler {
|
||||
lah := localapi.NewHandler(s.b, s.logf, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
@@ -1112,3 +1154,94 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
|
||||
}
|
||||
return ln.Listener.Accept()
|
||||
}
|
||||
|
||||
func findTaildropDir(dg distro.Distro) (string, error) {
|
||||
const name = "Taildrop"
|
||||
switch dg {
|
||||
case distro.Synology:
|
||||
return findSynologyTaildropDir(name)
|
||||
case distro.TrueNAS:
|
||||
return findTrueNASTaildropDir(name)
|
||||
}
|
||||
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
|
||||
}
|
||||
|
||||
// findSynologyTaildropDir looks for the first volume containing a
|
||||
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
|
||||
// but on DSM7 at least, we lack permissions to run that.
|
||||
func findSynologyTaildropDir(name string) (dir string, err error) {
|
||||
for i := 1; i <= 16; i++ {
|
||||
dir = fmt.Sprintf("/volume%v/%s", i, name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findTrueNASTaildropDir returns the first matching directory of
|
||||
// /mnt/{name} or /mnt/*/{name}
|
||||
func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
|
||||
dir = fmt.Sprintf("/mnt/%s", name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// but if running on the host, it may be something like /mnt/Primary/Taildrop
|
||||
fis, err := ioutil.ReadDir("/mnt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading /mnt: %w", err)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
func loadExtraEnv() (env []string, err error) {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil, nil
|
||||
}
|
||||
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
|
||||
contents, err := os.ReadFile(name)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(contents), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k, v, ok := stringsCut(line, "=")
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(v, `"`) {
|
||||
var err error
|
||||
v, err = strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
|
||||
}
|
||||
env = append(env, k+"="+v)
|
||||
} else {
|
||||
env = append(env, line)
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// stringsCut is Go 1.18's strings.Cut.
|
||||
// TODO(bradfitz): delete this when we depend on Go 1.18.
|
||||
func stringsCut(s, sep string) (before, after string, found bool) {
|
||||
if i := strings.Index(s, sep); i >= 0 {
|
||||
return s[:i], s[i+len(sep):], true
|
||||
}
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -32,10 +33,11 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
|
||||
s := safesocket.DefaultConnectionStrategy(socketPath)
|
||||
connect := func() {
|
||||
for i := 1; i <= 2; i++ {
|
||||
logf("connect %d ...", i)
|
||||
c, err := safesocket.Connect(socketPath, 0)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
t.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
@@ -72,6 +74,6 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ type Status struct {
|
||||
// "Starting", "Running".
|
||||
BackendState string
|
||||
|
||||
// TailnetName is the name of the network that's currently in
|
||||
// use.
|
||||
TailnetName string
|
||||
|
||||
AuthURL string // current URL provided by control to authorize client
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
@@ -70,9 +74,14 @@ func (s *Status) Peers() []key.NodePublic {
|
||||
}
|
||||
|
||||
type PeerStatusLite struct {
|
||||
// TxBytes/RxBytes is the total number of bytes transmitted to/received from this peer.
|
||||
TxBytes, RxBytes int64
|
||||
LastHandshake time.Time
|
||||
NodeKey key.NodePublic
|
||||
// LastHandshake is the last time a handshake succeeded with this peer.
|
||||
// (Or we got key confirmation via the first data message,
|
||||
// which is approximately the same thing.)
|
||||
LastHandshake time.Time
|
||||
// NodeKey is this peer's public node key.
|
||||
NodeKey key.NodePublic
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
@@ -83,22 +92,23 @@ type PeerStatus struct {
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
|
||||
// Endpoints:
|
||||
Addrs []string
|
||||
CurAddr string // one of Addrs, or unique if roaming
|
||||
Relay string // DERP region
|
||||
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
||||
LastHandshake time.Time // with local wireguard
|
||||
Online bool // whether node is connected to the control plane
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||
|
||||
// Active is whether the node was recently active. The
|
||||
// definition is somewhat undefined but has historically and
|
||||
@@ -237,9 +247,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if v := st.UserID; v != 0 {
|
||||
e.UserID = v
|
||||
}
|
||||
if v := st.TailAddrDeprecated; v != "" {
|
||||
e.TailAddrDeprecated = v
|
||||
}
|
||||
if v := st.TailscaleIPs; v != nil {
|
||||
e.TailscaleIPs = v
|
||||
}
|
||||
@@ -270,6 +277,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if v := st.LastWrite; !v.IsZero() {
|
||||
e.LastWrite = v
|
||||
}
|
||||
if st.Online {
|
||||
e.Online = true
|
||||
}
|
||||
if st.InNetworkMap {
|
||||
e.InNetworkMap = true
|
||||
}
|
||||
@@ -285,6 +295,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
if st.ExitNodeOption {
|
||||
e.ExitNodeOption = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -63,10 +63,10 @@ func (h *Handler) certDir() (string, error) {
|
||||
return full, nil
|
||||
}
|
||||
|
||||
var acmeDebug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ACME"))
|
||||
var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
if !h.PermitWrite && !h.PermitCert {
|
||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user