Compare commits
477 Commits
upnpdebug
...
danderson/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743293d473 | ||
|
|
2486d7cb9b | ||
|
|
ef241f782e | ||
|
|
073a3ec416 | ||
|
|
cb87b7aa5b | ||
|
|
06dccea416 | ||
|
|
05cc2f510b | ||
|
|
05e55f4a0b | ||
|
|
55b6753c11 | ||
|
|
429632d32c | ||
|
|
c1d009b9e9 | ||
|
|
ebae0d95d0 | ||
|
|
ef14663934 | ||
|
|
94f6257fde | ||
|
|
1f06f77dcb | ||
|
|
37c150aee1 | ||
|
|
15376f975b | ||
|
|
19189d7018 | ||
|
|
c41fe182f0 | ||
|
|
4d38194c21 | ||
|
|
e03fda7ae6 | ||
|
|
7c40a5d440 | ||
|
|
ada8cd99af | ||
|
|
94fb42d4b2 | ||
|
|
1df865a580 | ||
|
|
c1d377078d | ||
|
|
4bb2c6980d | ||
|
|
640de1921f | ||
|
|
aad46bd9ff | ||
|
|
c9bf773312 | ||
|
|
d36c0d3566 | ||
|
|
6e5175373e | ||
|
|
96ad68c5d6 | ||
|
|
bab2d92c42 | ||
|
|
3164c7410e | ||
|
|
0c546a28ba | ||
|
|
5302e4be96 | ||
|
|
dc2fbf5877 | ||
|
|
7b87c04861 | ||
|
|
3ad11f6b8c | ||
|
|
31e4f60047 | ||
|
|
a9c78910bd | ||
|
|
a47158e14d | ||
|
|
bc89a796ec | ||
|
|
22dbaa0894 | ||
|
|
d381bc2b6c | ||
|
|
c23a378f63 | ||
|
|
e4d2ef2b67 | ||
|
|
cf8fcc1254 | ||
|
|
869999955d | ||
|
|
f27950e97f | ||
|
|
060ba86baa | ||
|
|
675f9cd199 | ||
|
|
4a65b07e34 | ||
|
|
5df7ac70d6 | ||
|
|
2ce5fc7b0a | ||
|
|
3b5ada1fd8 | ||
|
|
75de4e9cc2 | ||
|
|
b0b0a80318 | ||
|
|
eebe7afad7 | ||
|
|
81cabf48ec | ||
|
|
139a6c4c9c | ||
|
|
a320d70614 | ||
|
|
04d24d3a38 | ||
|
|
422ea4980f | ||
|
|
10745c099a | ||
|
|
85fa1b0d61 | ||
|
|
59a906df47 | ||
|
|
c1293b3858 | ||
|
|
505f844a43 | ||
|
|
0b62f26349 | ||
|
|
09e692e318 | ||
|
|
ed3fb197ad | ||
|
|
a8e2cceefd | ||
|
|
c209278a9b | ||
|
|
9b101bd6af | ||
|
|
c60806b557 | ||
|
|
9f954628e5 | ||
|
|
e25afc6656 | ||
|
|
8e3b8dbb50 | ||
|
|
6cb2705833 | ||
|
|
8efc306e4f | ||
|
|
9310713bfb | ||
|
|
0bf515e780 | ||
|
|
1b4e007425 | ||
|
|
7ce9c7ce84 | ||
|
|
118fe105f5 | ||
|
|
c30fa5903d | ||
|
|
3552d86525 | ||
|
|
eaa0aef934 | ||
|
|
b956139b0c | ||
|
|
7a243ae5b1 | ||
|
|
c6ea282b3f | ||
|
|
6425f497b1 | ||
|
|
11fdb14c53 | ||
|
|
e7eb46bced | ||
|
|
1c56643136 | ||
|
|
cb030a0bb4 | ||
|
|
53199738fb | ||
|
|
2aa5df7ac1 | ||
|
|
521b44e653 | ||
|
|
27799a1a96 | ||
|
|
a6d02dc122 | ||
|
|
c759fcc7d3 | ||
|
|
75a7779b42 | ||
|
|
9af27ba829 | ||
|
|
def650b3e8 | ||
|
|
f55c2bccf5 | ||
|
|
569f70abfd | ||
|
|
695df497ba | ||
|
|
04fd94acd6 | ||
|
|
151b4415ca | ||
|
|
d86081f353 | ||
|
|
e5779f019e | ||
|
|
36a07089ee | ||
|
|
3e80806804 | ||
|
|
82fa15fa3b | ||
|
|
7817ab6b20 | ||
|
|
2662a1c98c | ||
|
|
47ace13ac8 | ||
|
|
c6d3f622e9 | ||
|
|
e538d47bd5 | ||
|
|
4a3e2842d9 | ||
|
|
14f9c75293 | ||
|
|
ddf3394b40 | ||
|
|
77696579f5 | ||
|
|
7742caef0a | ||
|
|
2fa004a2a0 | ||
|
|
676fb458c3 | ||
|
|
a6c3de72d6 | ||
|
|
751c42c097 | ||
|
|
9ab8492694 | ||
|
|
45d4adcb63 | ||
|
|
061dab5d61 | ||
|
|
2c403cbb31 | ||
|
|
f01ff18b6f | ||
|
|
9795fca946 | ||
|
|
7dbb1b51fe | ||
|
|
c121fa81c4 | ||
|
|
1991a1ac6a | ||
|
|
4528f448d6 | ||
|
|
1b20d1ce54 | ||
|
|
5b06c50669 | ||
|
|
525f15bf81 | ||
|
|
9c3ae750da | ||
|
|
b382161fe5 | ||
|
|
92215065eb | ||
|
|
13ef8e3c06 | ||
|
|
a2e1e5d909 | ||
|
|
5d6198adee | ||
|
|
d883747d8b | ||
|
|
e5dddb2b99 | ||
|
|
3b0ee07713 | ||
|
|
af04726c18 | ||
|
|
d7a2828fed | ||
|
|
1f506d2351 | ||
|
|
8bdb2c3adc | ||
|
|
3675fafec6 | ||
|
|
297d1b7cb6 | ||
|
|
47044f3af7 | ||
|
|
7634af5c6f | ||
|
|
0d4a0bf60e | ||
|
|
52be1c0c78 | ||
|
|
830f641c6b | ||
|
|
2d11503cff | ||
|
|
2501a694cb | ||
|
|
df7899759d | ||
|
|
cc9cf97cbe | ||
|
|
0410f1a35a | ||
|
|
2a0d3f72c5 | ||
|
|
cb4a2c00d1 | ||
|
|
67e5fabdbd | ||
|
|
81269fad28 | ||
|
|
98b3fa78aa | ||
|
|
2d464cecd1 | ||
|
|
58e1475ec7 | ||
|
|
a71fb6428d | ||
|
|
22a1a5d7cf | ||
|
|
45f51d4fa6 | ||
|
|
6d43cbc325 | ||
|
|
babd163aac | ||
|
|
09c2462ae5 | ||
|
|
f62e6d83a9 | ||
|
|
7cf8ec8108 | ||
|
|
b10a55e4ed | ||
|
|
d7ce2be5f4 | ||
|
|
891e7986cc | ||
|
|
080381c79f | ||
|
|
3618e8d8ac | ||
|
|
b822b5c798 | ||
|
|
e016eaf410 | ||
|
|
3386a86fe5 | ||
|
|
5809386525 | ||
|
|
0fa1da2d1b | ||
|
|
173bbaa1a1 | ||
|
|
a7cb241db1 | ||
|
|
29a8fb45d3 | ||
|
|
56d8c2da34 | ||
|
|
8949305820 | ||
|
|
52737c14ac | ||
|
|
5263c8d0b5 | ||
|
|
3b3994f0db | ||
|
|
29fa8c17d2 | ||
|
|
7f0fcf8571 | ||
|
|
3a72ebb109 | ||
|
|
21e9f98fc1 | ||
|
|
82117f7a63 | ||
|
|
ec2249b6f2 | ||
|
|
5bc6d17f87 | ||
|
|
ace2faf7ec | ||
|
|
efb84ca60d | ||
|
|
27bc4e744c | ||
|
|
b7b7d21514 | ||
|
|
46b59e8c48 | ||
|
|
b0481ba37a | ||
|
|
9219ca49f5 | ||
|
|
9ca334a560 | ||
|
|
1eabb5b2d9 | ||
|
|
c350321eec | ||
|
|
2bb915dd0a | ||
|
|
aaea175dd0 | ||
|
|
eeee713c69 | ||
|
|
dbce536316 | ||
|
|
a56520c3c7 | ||
|
|
562622a32c | ||
|
|
4cf63b8df0 | ||
|
|
18086c4cb7 | ||
|
|
f0aa7f70a4 | ||
|
|
9ebb5d4205 | ||
|
|
b1a2abf41b | ||
|
|
478775de6a | ||
|
|
7d8227e7a6 | ||
|
|
2db877caa3 | ||
|
|
93c2882a2f | ||
|
|
280c84e46a | ||
|
|
aae622314e | ||
|
|
b14db5d943 | ||
|
|
3cd85c0ca6 | ||
|
|
d5a0a4297e | ||
|
|
d8a8f70000 | ||
|
|
367a973dc2 | ||
|
|
081be3e96b | ||
|
|
d5ab18b2e6 | ||
|
|
618376dbc0 | ||
|
|
fb66ff7c78 | ||
|
|
4da559d7cc | ||
|
|
a722e48cef | ||
|
|
07c09f470d | ||
|
|
f834a4ade5 | ||
|
|
221cc5f8f2 | ||
|
|
45d3174c0d | ||
|
|
b7ede14396 | ||
|
|
a05086ef86 | ||
|
|
4549d3151c | ||
|
|
73f177e4d5 | ||
|
|
d43fcd2f02 | ||
|
|
3ea8cf9c62 | ||
|
|
1b15349e01 | ||
|
|
3b58c118dd | ||
|
|
0f4c0e558b | ||
|
|
7421ba91ec | ||
|
|
5b02ad16b9 | ||
|
|
b681edc572 | ||
|
|
7693d36aed | ||
|
|
008f36986e | ||
|
|
9faee90744 | ||
|
|
4bbf5a8636 | ||
|
|
865d8c0d23 | ||
|
|
a3c5de641b | ||
|
|
a83f08c54b | ||
|
|
3e2a7de2e9 | ||
|
|
dabeda21e0 | ||
|
|
3759fb8987 | ||
|
|
0be26599ca | ||
|
|
0eb6cc9321 | ||
|
|
61f201f33d | ||
|
|
5a9d977c78 | ||
|
|
64e9ce8df1 | ||
|
|
4f648e6fcc | ||
|
|
382b349c54 | ||
|
|
31c1331415 | ||
|
|
a353fbd3b4 | ||
|
|
a76c8eea58 | ||
|
|
d5851d2e06 | ||
|
|
d8c5d00ecb | ||
|
|
de63e85810 | ||
|
|
12dc7c2df8 | ||
|
|
2238814b99 | ||
|
|
640134421e | ||
|
|
48bdffd395 | ||
|
|
cf855e8988 | ||
|
|
48933b0382 | ||
|
|
7fe6ecf165 | ||
|
|
90b0cd0c51 | ||
|
|
4c68b7df7c | ||
|
|
30a614f9b9 | ||
|
|
2bb0eb5f7e | ||
|
|
b2a3d1da13 | ||
|
|
9502b515f1 | ||
|
|
7bfd4f521d | ||
|
|
4917a96aec | ||
|
|
1d1efbb599 | ||
|
|
5a58fd8933 | ||
|
|
efe8020dfa | ||
|
|
000f90d4d7 | ||
|
|
69c897a763 | ||
|
|
bb6fdfb243 | ||
|
|
b3b1c06b3a | ||
|
|
10547d989d | ||
|
|
c071bcda33 | ||
|
|
980acc38ba | ||
|
|
61c3b98a24 | ||
|
|
4fdb88efe1 | ||
|
|
4ce091cbd8 | ||
|
|
d1cb7a2639 | ||
|
|
159d88aae7 | ||
|
|
b96159e820 | ||
|
|
99a1c74a6a | ||
|
|
db3586cd43 | ||
|
|
ec2b7c7da6 | ||
|
|
fc160f80ee | ||
|
|
5d800152d9 | ||
|
|
e4e4d336d9 | ||
|
|
4e18cca62e | ||
|
|
5bacbf3744 | ||
|
|
722942dd46 | ||
|
|
daf54d1253 | ||
|
|
39748e9562 | ||
|
|
954064bdfe | ||
|
|
f90ac11bd8 | ||
|
|
bb10443edf | ||
|
|
d00341360f | ||
|
|
dfd978f0f2 | ||
|
|
4c27e2fa22 | ||
|
|
1a899344bd | ||
|
|
b2181608b5 | ||
|
|
0842e2f45b | ||
|
|
f53792026e | ||
|
|
7f29dcaac1 | ||
|
|
fb8b821710 | ||
|
|
7a7aa8f2b0 | ||
|
|
3c8ca4b357 | ||
|
|
8744394cde | ||
|
|
21cb0b361f | ||
|
|
a59b389a6a | ||
|
|
0b9e938152 | ||
|
|
00b3c1c042 | ||
|
|
9b7fc2ed1f | ||
|
|
73280595a8 | ||
|
|
debaaebf3b | ||
|
|
a1f1020042 | ||
|
|
583af7c1a6 | ||
|
|
8668103f06 | ||
|
|
1a9fba5b04 | ||
|
|
0daa32943e | ||
|
|
44d71d1e42 | ||
|
|
f09ede9243 | ||
|
|
86d1c4eceb | ||
|
|
8bacfe6a37 | ||
|
|
e151b74f93 | ||
|
|
58c1f7d51a | ||
|
|
8049063d35 | ||
|
|
f2d949e2db | ||
|
|
fe2f89deab | ||
|
|
97693f2e42 | ||
|
|
61c62f48d9 | ||
|
|
54bc3b7d97 | ||
|
|
923c98cd8f | ||
|
|
065c4ffc2c | ||
|
|
09a47ea3f1 | ||
|
|
3f1317e3e5 | ||
|
|
30458c71c8 | ||
|
|
bb47feca44 | ||
|
|
fd4838dc57 | ||
|
|
7fcf86a14a | ||
|
|
0b962567fa | ||
|
|
4fcce70df5 | ||
|
|
0b2761ca92 | ||
|
|
ffd22050c0 | ||
|
|
5d3427021b | ||
|
|
a35c3ba221 | ||
|
|
83906abc5e | ||
|
|
ae9b3f38d6 | ||
|
|
baf8854f9a | ||
|
|
3606e68721 | ||
|
|
4aab083cae | ||
|
|
b49d9bc74d | ||
|
|
1925fb584e | ||
|
|
88bd796622 | ||
|
|
ac0353e982 | ||
|
|
780e65a613 | ||
|
|
37053801bb | ||
|
|
51976ab3a2 | ||
|
|
246fa67e56 | ||
|
|
6990a314f5 | ||
|
|
3ac731dda1 | ||
|
|
71b375c502 | ||
|
|
0ac2130590 | ||
|
|
c1aa5a2e33 | ||
|
|
f35b8c3ead | ||
|
|
fab296536c | ||
|
|
6731f934a6 | ||
|
|
47045265b9 | ||
|
|
4ff0757d44 | ||
|
|
1dd2552032 | ||
|
|
36ffd509de | ||
|
|
edb338f542 | ||
|
|
faa891c1f2 | ||
|
|
8269a23758 | ||
|
|
bf8556ab86 | ||
|
|
6ef734e493 | ||
|
|
adf696172d | ||
|
|
af30897f0d | ||
|
|
1f006025c2 | ||
|
|
fcca374fa7 | ||
|
|
cd426eaf4c | ||
|
|
9f62cc665e | ||
|
|
5c383bdf5d | ||
|
|
56db3e2548 | ||
|
|
6f8c8c771b | ||
|
|
b7ae529ecc | ||
|
|
e199e407d2 | ||
|
|
d5e1abd0c4 | ||
|
|
57b794c338 | ||
|
|
4c8b5fdec4 | ||
|
|
a666b546fb | ||
|
|
0c038b477f | ||
|
|
278e7de9c9 | ||
|
|
93284209bc | ||
|
|
8ab44b339e | ||
|
|
6da6d47a83 | ||
|
|
a24cee0d67 | ||
|
|
d2aa144dcc | ||
|
|
25e060a841 | ||
|
|
833200da6f | ||
|
|
e804ab29fd | ||
|
|
b2eea1ee00 | ||
|
|
39610aeb09 | ||
|
|
98d557dd24 | ||
|
|
3e7ff5ff98 | ||
|
|
954867fef5 | ||
|
|
c992504375 | ||
|
|
1bca722824 | ||
|
|
00b4c2331b | ||
|
|
9547669787 | ||
|
|
b5a41ff381 | ||
|
|
ec9f3f4cc0 | ||
|
|
c68a12afe9 | ||
|
|
d2d55bd63c | ||
|
|
c6740da624 | ||
|
|
7c7eb8094b | ||
|
|
5aba620fb9 | ||
|
|
b9bd7dbc5d | ||
|
|
26b6fe7f02 | ||
|
|
3700cf9ea4 | ||
|
|
5f45d8f8e6 | ||
|
|
a4e19f2233 | ||
|
|
bdb93c5942 | ||
|
|
26c1183941 | ||
|
|
0796c53404 | ||
|
|
8bdf878832 | ||
|
|
360223fccb | ||
|
|
4d19db7c9f | ||
|
|
e6d4ab2dd6 | ||
|
|
98d36ee18d | ||
|
|
85304d7392 | ||
|
|
777b711d96 | ||
|
|
5c98b1b8d0 | ||
|
|
eee6b85b9b | ||
|
|
a5da4ed981 | ||
|
|
a729070252 | ||
|
|
fd7b738e5b | ||
|
|
fdc081c291 | ||
|
|
f013960d87 | ||
|
|
f3c96df162 | ||
|
|
0858673f1f | ||
|
|
9d0c86b6ec |
1
.bencher/config.yaml
Normal file
1
.bencher/config.yaml
Normal file
@@ -0,0 +1 @@
|
||||
suppress_failure_on_regression: true
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,8 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [needs-triage, bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check if your bug is [already filed](https://github.com/tailscale/tailscale/issues).
|
||||
Have an urgent issue? Let us know by emailing us at <support@tailscale.com>.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What is the issue?
|
||||
description: What happened? What did you expect to happen?
|
||||
placeholder: oh no
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: What are the steps you took that hit this issue?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: changes
|
||||
attributes:
|
||||
label: Are there any recent changes that introduced the issue?
|
||||
description: If so, what are those changes?
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: OS
|
||||
description: What OS are you using? You may select more than one.
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- iOS
|
||||
- Android
|
||||
- Synology
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: OS version
|
||||
description: What OS version are you using?
|
||||
placeholder: e.g., Debian 11.0, macOS Big Sur 11.6, Synology DSM 7
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: ts-version
|
||||
attributes:
|
||||
label: Tailscale version
|
||||
description: What Tailscale version are you using?
|
||||
placeholder: e.g., 1.14.4
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: bug-report
|
||||
attributes:
|
||||
label: Bug report
|
||||
description: Please run [`tailscale bugreport`](https://tailscale.com/kb/1080/cli/?q=Cli#bugreport) and share the bug identifier. The identifier is a random string which allows Tailscale support to locate your account and gives a point to focus on when looking for errors.
|
||||
placeholder: e.g., BUG-1b7641a16971a9cd75822c0ed8043fee70ae88cf05c52981dc220eb96a5c49a8-20210427151443Z-fbcd4fd3a4b7ad94
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a bug report!
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Support and Product Questions
|
||||
- name: Support requests and Troubleshooting
|
||||
url: https://tailscale.com/kb/1023/troubleshooting
|
||||
about: Please send support questions and questions about the Tailscale product to support@tailscale.com
|
||||
about: Troubleshoot common issues. Contact us by email at support@tailscale.com.
|
||||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,7 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
---
|
||||
49
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Feature request
|
||||
description: Propose a new feature
|
||||
title: "FR: "
|
||||
labels: [needs-triage, fr]
|
||||
body:
|
||||
- type: markdown
|
||||
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
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What are you trying to do?
|
||||
description: Tell us about the problem you're trying to solve.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: How should we solve this?
|
||||
description: If you have an idea of how you'd like to see this feature work, let us know.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: What is the impact of not solving this?
|
||||
description: (How) Are you currently working around the issue?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: Any additional context to share, e.g., links
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a feature request!
|
||||
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Documentation for this file can be found at:
|
||||
# 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:"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: ".github:"
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release-branch/* ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '31 14 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
4
.github/workflows/cross-darwin.yml
vendored
4
.github/workflows/cross-darwin.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/cross-freebsd.yml
vendored
4
.github/workflows/cross-freebsd.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/cross-openbsd.yml
vendored
4
.github/workflows/cross-openbsd.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/cross-windows.yml
vendored
4
.github/workflows/cross-windows.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/depaware.yml
vendored
4
.github/workflows/depaware.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
14
.github/workflows/go_generate.yml
vendored
14
.github/workflows/go_generate.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
@@ -26,9 +26,13 @@ jobs:
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
mkdir gentools
|
||||
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
|
||||
PATH="$PATH:$(pwd)/gentools" go generate ./...
|
||||
if [[ "${{github.ref}}" == release-branch/* ]]
|
||||
then
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
else
|
||||
pkgs=$(go list ./... | grep -v dnsfallback)
|
||||
fi
|
||||
go generate $pkgs
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
||||
|
||||
4
.github/workflows/license.yml
vendored
4
.github/workflows/license.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
4
.github/workflows/linux-race.yml
vendored
4
.github/workflows/linux-race.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
4
.github/workflows/linux32.yml
vendored
4
.github/workflows/linux32.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
24
.github/workflows/staticcheck.yml
vendored
24
.github/workflows/staticcheck.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
@@ -31,16 +31,28 @@ jobs:
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: "386"
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
name: "integration-vms"
|
||||
name: VM
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "tstest/integration/vms/**"
|
||||
release:
|
||||
types: [ created ]
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
experimental-linux-vm-test:
|
||||
# To set up a new runner, see tstest/integration/vms/runner.nix
|
||||
runs-on: [ self-hosted, linux, vm_integration_test ]
|
||||
ubuntu2004-LTS-cloud-base:
|
||||
runs-on: [ self-hosted, linux, vm ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download VM Images
|
||||
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m -no-s3
|
||||
env:
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -run-vm-tests
|
||||
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
|
||||
env:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
4
.github/workflows/windows-race.yml
vendored
4
.github/workflows/windows-race.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.17.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
4
.github/workflows/windows.yml
vendored
4
.github/workflows/windows.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v2.1.4
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.17.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@@ -4,17 +4,11 @@
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
# WARNING: Tailscale is not yet officially supported in container
|
||||
# environments, such as Docker and Kubernetes. Though it should work, we
|
||||
# don't regularly test it, and we know there are some feature limitations.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# See current bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
@@ -23,11 +17,11 @@
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
#
|
||||
# $ docker build -t tailscale:tailscale .
|
||||
# $ docker build -t tailscale/tailscale .
|
||||
#
|
||||
# To run the tailscaled agent:
|
||||
#
|
||||
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale:tailscale tailscaled
|
||||
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale/tailscale tailscaled
|
||||
#
|
||||
# To then log in:
|
||||
#
|
||||
@@ -38,7 +32,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.16-alpine AS build-env
|
||||
FROM golang:1.17-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -59,8 +53,8 @@ RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/...
|
||||
-v ./cmd/tailscale ./cmd/tailscaled
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
5
Makefile
5
Makefile
@@ -18,7 +18,10 @@ buildwindows:
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -43,7 +43,7 @@ If your distro has conventions that preclude the use of
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.16) in module mode. It might
|
||||
release candidate builds (currently Go 1.17) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.13.0
|
||||
1.17.0
|
||||
|
||||
86
api.md
86
api.md
@@ -3,7 +3,7 @@
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
|
||||
|
||||
# APIs
|
||||
|
||||
@@ -13,11 +13,14 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
- Authorize machine
|
||||
- [POST device authorized](#device-authorized-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [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)
|
||||
- [DNS](#tailnet-dns)
|
||||
@@ -37,7 +40,7 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
<a name=device-get></a>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
@@ -126,7 +129,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
<a name=device-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
@@ -163,7 +166,7 @@ If the device is not owned by your tailnet:
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></div>
|
||||
<a name=device-routes-get></a>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
@@ -192,7 +195,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
<a name=device-routes-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
@@ -233,6 +236,33 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-authorized-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/authorized` - authorize a device
|
||||
|
||||
Marks a device as authorized, for Tailnets where device authorization is required.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`authorized` - whether the device is authorized; only `true` is currently supported.
|
||||
```
|
||||
{
|
||||
"authorized": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/authorized' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"authorized": 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.
|
||||
@@ -473,7 +503,7 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
###### Query Parameters
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this paramater to determine which rules match.
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
@@ -510,6 +540,50 @@ Response:
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-validate-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
|
||||
|
||||
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The POST body should be a JSON formatted array of ACL Tests.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
{
|
||||
[
|
||||
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
If all the tests pass, the response will be empty, with an http status code of 200.
|
||||
|
||||
Failed test error response:
|
||||
A 400 http status code and the errors in the response body.
|
||||
```
|
||||
{
|
||||
"message":"test(s) failed",
|
||||
"data":[
|
||||
{
|
||||
"user":"user1@example.com",
|
||||
"errors":["address \"2.2.2.2:22\": want: Drop, got: Accept"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
@@ -8,17 +8,11 @@
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
# WARNING: Tailscale is not yet officially supported in container
|
||||
# environments, such as Docker and Kubernetes. Though it should work, we
|
||||
# don't regularly test it, and we know there are some feature limitations.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# See current bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
@@ -31,4 +25,4 @@ docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:tailscale .
|
||||
-t tailscale:$VERSION_SHORT -t tailscale:latest .
|
||||
|
||||
83
chirp/chirp.go
Normal file
83
chirp/chirp.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 chirp implements a client to communicate with the BIRD Internet
|
||||
// Routing Daemon.
|
||||
package chirp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// New creates a BIRDClient.
|
||||
func New(socket string) (*BIRDClient, error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
b := &BIRDClient{socket: socket, conn: conn, bs: bufio.NewScanner(conn)}
|
||||
// Read and discard the first line as that is the welcome message.
|
||||
if _, err := b.readLine(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
|
||||
type BIRDClient struct {
|
||||
socket string
|
||||
conn net.Conn
|
||||
bs *bufio.Scanner
|
||||
}
|
||||
|
||||
// Close closes the underlying connection to BIRD.
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(out, fmt.Sprintf("%s: already disabled", protocol)) {
|
||||
return nil
|
||||
} else if strings.Contains(out, fmt.Sprintf("%s: disabled", protocol)) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to disable %s: %v", protocol, out)
|
||||
}
|
||||
|
||||
// EnableProtocol enables the provided protocol.
|
||||
func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
out, err := b.exec("enable %s\n", protocol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(out, fmt.Sprintf("%s: already enabled", protocol)) {
|
||||
return nil
|
||||
} else if strings.Contains(out, fmt.Sprintf("%s: enabled", protocol)) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to enable %s: %v", protocol, out)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readLine() (string, error) {
|
||||
if !b.bs.Scan() {
|
||||
return "", fmt.Errorf("reading response from bird failed")
|
||||
}
|
||||
if err := b.bs.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.bs.Text(), nil
|
||||
}
|
||||
29
client/tailscale/example/servetls/servetls.go
Normal file
29
client/tailscale/example/servetls/servetls.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 servetls program shows how to run an HTTPS server
|
||||
// using a Tailscale cert via LetsEncrypt.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "<h1>Hello from Tailscale!</h1> It works.")
|
||||
}),
|
||||
}
|
||||
log.Printf("Running TLS server on :443 ...")
|
||||
log.Fatal(s.ListenAndServeTLS("", ""))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package tailscale
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -16,41 +17,56 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
var (
|
||||
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
|
||||
TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
},
|
||||
},
|
||||
// TailscaledDialer is the DialContext func that connects to the local machine's
|
||||
// tailscaled or equivalent.
|
||||
TailscaledDialer = defaultDialer
|
||||
)
|
||||
|
||||
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() {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
}
|
||||
|
||||
var (
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
// We lazily initialize the client in case the caller wants to
|
||||
// override TailscaledDialer.
|
||||
tsClient *http.Client
|
||||
tsClientOnce sync.Once
|
||||
)
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
//
|
||||
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
|
||||
@@ -61,6 +77,13 @@ var tsClient = &http.Client{
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
tsClientOnce.Do(func() {
|
||||
tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: TailscaledDialer,
|
||||
},
|
||||
}
|
||||
})
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
@@ -71,6 +94,20 @@ type errorJSON struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// AccessDeniedError is an error due to permissions.
|
||||
type AccessDeniedError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
|
||||
func (e *AccessDeniedError) Unwrap() error { return e.err }
|
||||
|
||||
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
|
||||
func IsAccessDeniedError(err error) bool {
|
||||
var ae *AccessDeniedError
|
||||
return errors.As(err, &ae)
|
||||
}
|
||||
|
||||
// bestError returns either err, or if body contains a valid JSON
|
||||
// object of type errorJSON, its non-empty error body.
|
||||
func bestError(err error, body []byte) error {
|
||||
@@ -81,6 +118,23 @@ func bestError(err error, body []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func errorMessageFromBody(body []byte) string {
|
||||
var j errorJSON
|
||||
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
|
||||
return j.Error
|
||||
}
|
||||
return strings.TrimSpace(string(body))
|
||||
}
|
||||
|
||||
var onVersionMismatch func(clientVer, serverVer string)
|
||||
|
||||
// SetVersionMismatchHandler sets f as the version mismatch handler
|
||||
// to be called when the client (the current process) has a version
|
||||
// number that doesn't match the server's declared version.
|
||||
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
||||
onVersionMismatch = f
|
||||
}
|
||||
|
||||
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
||||
if err != nil {
|
||||
@@ -88,14 +142,29 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
}
|
||||
res, err := DoLocalRequest(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)
|
||||
}
|
||||
@@ -127,6 +196,18 @@ func Goroutines(ctx context.Context) ([]byte, error) {
|
||||
return get200(ctx, "/localapi/v0/goroutines")
|
||||
}
|
||||
|
||||
// Profile returns a pprof profile of the Tailscale daemon.
|
||||
func Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
|
||||
var secArg string
|
||||
if sec < 0 || sec > 300 {
|
||||
return nil, errors.New("duration out of range")
|
||||
}
|
||||
if sec != 0 || pprofType == "profile" {
|
||||
secArg = fmt.Sprint(sec)
|
||||
}
|
||||
return get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
func BugReport(ctx context.Context, note string) (string, error) {
|
||||
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
||||
@@ -293,3 +374,99 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
}
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// with ?type=pair, the response PEM is first the one private
|
||||
// key PEM block, then the cert PEM blocks.
|
||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||
if i == -1 {
|
||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||
}
|
||||
i += len("--\n")
|
||||
keyPEM, certPEM = res[:i], res[i:]
|
||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// tls.Config.GetCertificate.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
name := hi.ServerName
|
||||
if !strings.Contains(name, ".") {
|
||||
if v, ok := ExpandSNIName(ctx, name); ok {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
certPEM, keyPEM, err := CertPair(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
||||
// platform equivalent) is not answering localapi connections.
|
||||
//
|
||||
// It ends in a punctuation. See caller.
|
||||
func tailscaledConnectHint() string {
|
||||
if runtime.GOOS != "linux" {
|
||||
// TODO(bradfitz): flesh this out
|
||||
return "not running?"
|
||||
}
|
||||
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
|
||||
if err != nil {
|
||||
return "not running?"
|
||||
}
|
||||
// Parse:
|
||||
// LoadState=loaded
|
||||
// ActiveState=inactive
|
||||
// SubState=dead
|
||||
st := map[string]string{}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if i := strings.Index(line, "="); i != -1 {
|
||||
st[line[:i]] = strings.TrimSpace(line[i+1:])
|
||||
}
|
||||
}
|
||||
if st["LoadState"] == "loaded" &&
|
||||
(st["SubState"] != "running" || st["ActiveState"] != "active") {
|
||||
return "systemd tailscaled.service not running."
|
||||
}
|
||||
return "not running?"
|
||||
}
|
||||
|
||||
@@ -17,16 +17,13 @@ import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"tailscale.com/util/codegen"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -63,37 +60,13 @@ func main() {
|
||||
pkg := pkgs[0]
|
||||
buf := new(bytes.Buffer)
|
||||
imports := make(map[string]struct{})
|
||||
namedTypes := codegen.NamedTypes(pkg)
|
||||
for _, typeName := range typeNames {
|
||||
found := false
|
||||
for _, file := range pkg.Syntax {
|
||||
//var fbuf bytes.Buffer
|
||||
//ast.Fprint(&fbuf, pkg.Fset, file, nil)
|
||||
//fmt.Println(fbuf.String())
|
||||
|
||||
for _, d := range file.Decls {
|
||||
decl, ok := d.(*ast.GenDecl)
|
||||
if !ok || decl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
for _, s := range decl.Specs {
|
||||
spec, ok := s.(*ast.TypeSpec)
|
||||
if !ok || spec.Name.Name != typeName {
|
||||
continue
|
||||
}
|
||||
typeNameObj := pkg.TypesInfo.Defs[spec.Name]
|
||||
typ, ok := typeNameObj.Type().(*types.Named)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pkg := typeNameObj.Pkg()
|
||||
gen(buf, imports, typeName, typ, pkg)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
typ, ok := namedTypes[typeName]
|
||||
if !ok {
|
||||
log.Fatalf("could not find type %s", typeName)
|
||||
}
|
||||
gen(buf, imports, typ, pkg.Types)
|
||||
}
|
||||
|
||||
w := func(format string, args ...interface{}) {
|
||||
@@ -122,7 +95,20 @@ func main() {
|
||||
}
|
||||
|
||||
contents := new(bytes.Buffer)
|
||||
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
|
||||
var flagArgs []string
|
||||
if *flagTypes != "" {
|
||||
flagArgs = append(flagArgs, "-type="+*flagTypes)
|
||||
}
|
||||
if *flagOutput != "" {
|
||||
flagArgs = append(flagArgs, "-output="+*flagOutput)
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
|
||||
}
|
||||
if *flagCloneFunc {
|
||||
flagArgs = append(flagArgs, "-clonefunc")
|
||||
}
|
||||
fmt.Fprintf(contents, header, strings.Join(flagArgs, " "), pkg.Name)
|
||||
fmt.Fprintf(contents, "import (\n")
|
||||
for s := range imports {
|
||||
fmt.Fprintf(contents, "\t%q\n", s)
|
||||
@@ -130,17 +116,12 @@ func main() {
|
||||
fmt.Fprintf(contents, ")\n\n")
|
||||
contents.Write(buf.Bytes())
|
||||
|
||||
out, err := format.Source(contents.Bytes())
|
||||
if err != nil {
|
||||
log.Fatalf("%s, in source:\n%s", err, contents.Bytes())
|
||||
}
|
||||
|
||||
output := *flagOutput
|
||||
if output == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := ioutil.WriteFile(output, out, 0644); err != nil {
|
||||
if err := codegen.WriteFormatted(contents.Bytes(), output); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -149,13 +130,14 @@ const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserve
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type %s; DO NOT EDIT.
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//` + `go:generate` + ` go run tailscale.com/cmd/cloner %s
|
||||
|
||||
package %s
|
||||
|
||||
`
|
||||
|
||||
func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types.Named, thisPkg *types.Package) {
|
||||
func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisPkg *types.Package) {
|
||||
pkgQual := func(pkg *types.Package) string {
|
||||
if thisPkg == pkg {
|
||||
return ""
|
||||
@@ -167,107 +149,89 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
|
||||
return types.TypeString(t, pkgQual)
|
||||
}
|
||||
|
||||
switch t := typ.Underlying().(type) {
|
||||
case *types.Struct:
|
||||
// We generate two bits of code simultaneously while we walk the struct.
|
||||
// One is the Clone method itself, which we write directly to buf.
|
||||
// The other is a variable assignment that will fail if the struct
|
||||
// changes without the Clone method getting regenerated.
|
||||
// We write that to regenBuf, and then append it to buf at the end.
|
||||
regenBuf := new(bytes.Buffer)
|
||||
writeRegen := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(regenBuf, format+"\n", args...)
|
||||
}
|
||||
writeRegen("// A compilation failure here means this code must be regenerated, with command:")
|
||||
writeRegen("// tailscale.com/cmd/cloner -type %s", *flagTypes)
|
||||
writeRegen("var _%sNeedsRegeneration = %s(struct {", name, name)
|
||||
|
||||
name := typ.Obj().Name()
|
||||
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
|
||||
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
|
||||
writef := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(buf, "\t"+format+"\n", args...)
|
||||
}
|
||||
writef("if src == nil {")
|
||||
writef("\treturn nil")
|
||||
writef("}")
|
||||
writef("dst := new(%s)", name)
|
||||
writef("*dst = *src")
|
||||
for i := 0; i < t.NumFields(); i++ {
|
||||
fname := t.Field(i).Name()
|
||||
ft := t.Field(i).Type()
|
||||
|
||||
writeRegen("\t%s %s", fname, importedName(ft))
|
||||
|
||||
if !containsPointers(ft) {
|
||||
continue
|
||||
}
|
||||
if named, _ := ft.(*types.Named); named != nil && !hasBasicUnderlying(ft) {
|
||||
writef("dst.%s = *src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
switch ft := ft.Underlying().(type) {
|
||||
case *types.Slice:
|
||||
if containsPointers(ft.Elem()) {
|
||||
n := importedName(ft.Elem())
|
||||
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
|
||||
writef("for i := range dst.%s {", fname)
|
||||
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
writef("}")
|
||||
} else {
|
||||
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
|
||||
}
|
||||
case *types.Pointer:
|
||||
if named, _ := ft.Elem().(*types.Named); named != nil && containsPointers(ft.Elem()) {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
n := importedName(ft.Elem())
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = new(%s)", fname, n)
|
||||
writef("\t*dst.%s = *src.%s", fname, fname)
|
||||
if containsPointers(ft.Elem()) {
|
||||
writef("\t" + `panic("TODO pointers in pointers")`)
|
||||
}
|
||||
writef("}")
|
||||
case *types.Map:
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
|
||||
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
|
||||
n := importedName(sliceType.Elem())
|
||||
writef("\tfor k := range src.%s {", fname)
|
||||
// use zero-length slice instead of nil to ensure
|
||||
// the key is always copied.
|
||||
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
|
||||
writef("\t}")
|
||||
} else if containsPointers(ft.Elem()) {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
writef("\t}")
|
||||
} else {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v", fname)
|
||||
writef("\t}")
|
||||
}
|
||||
writef("}")
|
||||
case *types.Struct:
|
||||
writef(`panic("TODO struct %s")`, fname)
|
||||
default:
|
||||
writef(`panic(fmt.Sprintf("TODO: %T", ft))`)
|
||||
}
|
||||
}
|
||||
writef("return dst")
|
||||
fmt.Fprintf(buf, "}\n\n")
|
||||
|
||||
writeRegen("}{})\n")
|
||||
|
||||
buf.Write(regenBuf.Bytes())
|
||||
t, ok := typ.Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
name := typ.Obj().Name()
|
||||
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
|
||||
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
|
||||
writef := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(buf, "\t"+format+"\n", args...)
|
||||
}
|
||||
writef("if src == nil {")
|
||||
writef("\treturn nil")
|
||||
writef("}")
|
||||
writef("dst := new(%s)", name)
|
||||
writef("*dst = *src")
|
||||
for i := 0; i < t.NumFields(); i++ {
|
||||
fname := t.Field(i).Name()
|
||||
ft := t.Field(i).Type()
|
||||
if !codegen.ContainsPointers(ft) {
|
||||
continue
|
||||
}
|
||||
if named, _ := ft.(*types.Named); named != nil && !hasBasicUnderlying(ft) {
|
||||
writef("dst.%s = *src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
switch ft := ft.Underlying().(type) {
|
||||
case *types.Slice:
|
||||
if codegen.ContainsPointers(ft.Elem()) {
|
||||
n := importedName(ft.Elem())
|
||||
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
|
||||
writef("for i := range dst.%s {", fname)
|
||||
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
writef("}")
|
||||
} else {
|
||||
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
|
||||
}
|
||||
case *types.Pointer:
|
||||
if named, _ := ft.Elem().(*types.Named); named != nil && codegen.ContainsPointers(ft.Elem()) {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
n := importedName(ft.Elem())
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = new(%s)", fname, n)
|
||||
writef("\t*dst.%s = *src.%s", fname, fname)
|
||||
if codegen.ContainsPointers(ft.Elem()) {
|
||||
writef("\t" + `panic("TODO pointers in pointers")`)
|
||||
}
|
||||
writef("}")
|
||||
case *types.Map:
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
|
||||
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
|
||||
n := importedName(sliceType.Elem())
|
||||
writef("\tfor k := range src.%s {", fname)
|
||||
// use zero-length slice instead of nil to ensure
|
||||
// the key is always copied.
|
||||
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
|
||||
writef("\t}")
|
||||
} else if codegen.ContainsPointers(ft.Elem()) {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
writef("\t}")
|
||||
} else {
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
writef("\t\tdst.%s[k] = v", fname)
|
||||
writef("\t}")
|
||||
}
|
||||
writef("}")
|
||||
default:
|
||||
writef(`panic("TODO: %s (%T)")`, fname, ft)
|
||||
}
|
||||
}
|
||||
writef("return dst")
|
||||
fmt.Fprintf(buf, "}\n\n")
|
||||
|
||||
buf.Write(codegen.AssertStructUnchanged(t, thisPkg, name, "Clone", imports))
|
||||
}
|
||||
|
||||
func hasBasicUnderlying(typ types.Type) bool {
|
||||
@@ -278,34 +242,3 @@ func hasBasicUnderlying(typ types.Type) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func containsPointers(typ types.Type) bool {
|
||||
switch typ.String() {
|
||||
case "time.Time":
|
||||
// time.Time contains a pointer that does not need copying
|
||||
return false
|
||||
case "inet.af/netaddr.IP":
|
||||
return false
|
||||
}
|
||||
switch ft := typ.Underlying().(type) {
|
||||
case *types.Array:
|
||||
return containsPointers(ft.Elem())
|
||||
case *types.Chan:
|
||||
return true
|
||||
case *types.Interface:
|
||||
return true // a little too broad
|
||||
case *types.Map:
|
||||
return true
|
||||
case *types.Pointer:
|
||||
return true
|
||||
case *types.Slice:
|
||||
return true
|
||||
case *types.Struct:
|
||||
for i := 0; i < ft.NumFields(); i++ {
|
||||
if containsPointers(ft.Field(i).Type()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
95
cmd/derper/cert.go
Normal file
95
cmd/derper/cert.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 main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||
|
||||
type certProvider interface {
|
||||
// TLSConfig creates a new TLS config suitable for net/http.Server servers.
|
||||
TLSConfig() *tls.Config
|
||||
// HTTPHandler handle ACME related request, if any.
|
||||
HTTPHandler(fallback http.Handler) http.Handler
|
||||
}
|
||||
|
||||
func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
|
||||
if dir == "" {
|
||||
return nil, errors.New("missing required --certdir flag")
|
||||
}
|
||||
switch mode {
|
||||
case "letsencrypt":
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(hostname),
|
||||
Cache: autocert.DirCache(dir),
|
||||
}
|
||||
if hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
return certManager, nil
|
||||
case "manual":
|
||||
return NewManualCertManager(dir, hostname)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport cert mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
type manualCertManager struct {
|
||||
cert *tls.Certificate
|
||||
hostname string
|
||||
}
|
||||
|
||||
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
|
||||
func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
keyname := unsafeHostnameCharacters.ReplaceAllString(hostname, "")
|
||||
crtPath := filepath.Join(certdir, keyname+".crt")
|
||||
keyPath := filepath.Join(certdir, keyname+".key")
|
||||
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
||||
}
|
||||
// ensure hostname matches with the certificate
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
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")
|
||||
}
|
||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: nil,
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
},
|
||||
GetCertificate: m.getCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi.ServerName != m.hostname {
|
||||
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
||||
}
|
||||
return m.cert, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return fallback
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -32,13 +31,13 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
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")
|
||||
@@ -49,13 +48,33 @@ var (
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
)
|
||||
|
||||
var (
|
||||
stats = new(metrics.Set)
|
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"}
|
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"}
|
||||
|
||||
stunReadError = stunDisposition.Get("read_error")
|
||||
stunNotSTUN = stunDisposition.Get("not_stun")
|
||||
stunWriteError = stunDisposition.Get("write_error")
|
||||
stunSuccess = stunDisposition.Get("success")
|
||||
|
||||
stunIPv4 = stunAddrFamily.Get("ipv4")
|
||||
stunIPv6 = stunAddrFamily.Get("ipv6")
|
||||
)
|
||||
|
||||
func init() {
|
||||
stats.Set("counter_requests", stunDisposition)
|
||||
stats.Set("counter_addrfamily", stunAddrFamily)
|
||||
expvar.Publish("stun", stats)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey key.NodePrivate
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
if *dev {
|
||||
return config{PrivateKey: mustNewKey()}
|
||||
return config{PrivateKey: key.NewNode()}
|
||||
}
|
||||
if *configPath == "" {
|
||||
if os.Getuid() == 0 {
|
||||
@@ -81,21 +100,13 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func writeNewConfig() config {
|
||||
key := mustNewKey()
|
||||
k := key.NewNode()
|
||||
if err := os.MkdirAll(filepath.Dir(*configPath), 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cfg := config{
|
||||
PrivateKey: key,
|
||||
PrivateKey: k,
|
||||
}
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
@@ -117,6 +128,11 @@ func main() {
|
||||
tsweb.DevMode = true
|
||||
}
|
||||
|
||||
listenHost, _, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid server address: %v", err)
|
||||
}
|
||||
|
||||
var logPol *logpolicy.Policy
|
||||
if *logCollection != "" {
|
||||
logPol = logpolicy.New(*logCollection)
|
||||
@@ -125,9 +141,9 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
serveTLS := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
@@ -148,7 +164,10 @@ func main() {
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
derpHandler := derphttp.Handler(s)
|
||||
derpHandler = addWebSocketSupport(s, derpHandler)
|
||||
mux.Handle("/derp", derpHandler)
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -181,33 +200,35 @@ func main() {
|
||||
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
|
||||
|
||||
if *runSTUN {
|
||||
go serveSTUN()
|
||||
go serveSTUN(listenHost)
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
|
||||
// Set read/write timeout. For derper, this basically
|
||||
// only affects TLS setup, as read/write deadlines are
|
||||
// cleared on Hijack, which the DERP server does. But
|
||||
// without this, we slowly accumulate stuck TLS
|
||||
// handshake goroutines forever. This also affects
|
||||
// /debug/ traffic, but 30 seconds is plenty for
|
||||
// Prometheus/etc scraping.
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
var err error
|
||||
if letsEncrypt {
|
||||
if *certDir == "" {
|
||||
log.Fatalf("missing required --certdir flag")
|
||||
}
|
||||
if serveTLS {
|
||||
log.Printf("derper: serving on %s with TLS", *addr)
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(*hostname),
|
||||
Cache: autocert.DirCache(*certDir),
|
||||
}
|
||||
if *hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
var certManager certProvider
|
||||
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("derper: can not start cert provider: %v", err)
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
||||
getCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := letsEncryptGetCert(hi)
|
||||
cert, err := getCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,7 +236,17 @@ func main() {
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
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)
|
||||
@@ -232,45 +263,44 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func serveSTUN() {
|
||||
pc, err := net.ListenPacket("udp", ":3478")
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "HEAD", "GET":
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func serveSTUN(host string) {
|
||||
pc, err := net.ListenPacket("udp", net.JoinHostPort(host, "3478"))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open STUN listener: %v", err)
|
||||
}
|
||||
log.Printf("running STUN server on %v", pc.LocalAddr())
|
||||
serverSTUNListener(context.Background(), pc.(*net.UDPConn))
|
||||
}
|
||||
|
||||
var (
|
||||
stats = new(metrics.Set)
|
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"}
|
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"}
|
||||
|
||||
stunReadError = stunDisposition.Get("read_error")
|
||||
stunNotSTUN = stunDisposition.Get("not_stun")
|
||||
stunWriteError = stunDisposition.Get("write_error")
|
||||
stunSuccess = stunDisposition.Get("success")
|
||||
|
||||
stunIPv4 = stunAddrFamily.Get("ipv4")
|
||||
stunIPv6 = stunAddrFamily.Get("ipv6")
|
||||
)
|
||||
stats.Set("counter_requests", stunDisposition)
|
||||
stats.Set("counter_addrfamily", stunAddrFamily)
|
||||
expvar.Publish("stun", stats)
|
||||
|
||||
func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
|
||||
var buf [64 << 10]byte
|
||||
var (
|
||||
n int
|
||||
ua *net.UDPAddr
|
||||
err error
|
||||
)
|
||||
for {
|
||||
n, addr, err := pc.ReadFrom(buf[:])
|
||||
n, ua, err = pc.ReadFromUDP(buf[:])
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("STUN ReadFrom: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
stunReadError.Add(1)
|
||||
continue
|
||||
}
|
||||
ua, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Printf("STUN unexpected address %T %v", addr, addr)
|
||||
stunReadError.Add(1)
|
||||
continue
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if !stun.Is(pkt) {
|
||||
stunNotSTUN.Add(1)
|
||||
@@ -287,7 +317,7 @@ func serveSTUN() {
|
||||
stunIPv6.Add(1)
|
||||
}
|
||||
res := stun.Response(txid, ua.IP, uint16(ua.Port))
|
||||
_, err = pc.WriteTo(res, addr)
|
||||
_, err = pc.WriteTo(res, ua)
|
||||
if err != nil {
|
||||
stunWriteError.Add(1)
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
)
|
||||
|
||||
func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
@@ -31,5 +34,36 @@ func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
t.Errorf("f(%q) = %v; want %v", tt.in, got, tt.wantOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkServerSTUN(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer pc.Close()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go serverSTUNListener(ctx, pc.(*net.UDPConn))
|
||||
addr := pc.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
var resBuf [1500]byte
|
||||
cc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
tx := stun.NewTxID()
|
||||
req := stun.Request(tx)
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := cc.WriteToUDP(req, addr); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_, _, err := cc.ReadFromUDP(resBuf[:])
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
add := func(k key.NodePublic) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
52
cmd/derper/websocket.go
Normal file
52
cmd/derper/websocket.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"expvar"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
|
||||
// addWebSocketSupport returns a Handle wrapping base that adds WebSocket server support.
|
||||
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
|
||||
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
|
||||
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
|
||||
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
|
||||
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
|
||||
base.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
OriginPatterns: []string{"*"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket.Accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "closing")
|
||||
if c.Subprotocol() != "derp" {
|
||||
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
|
||||
return
|
||||
}
|
||||
counterWebSocketAccepts.Add(1)
|
||||
wc := wsconn.New(c)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(wc, brw, r.RemoteAddr)
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -67,22 +69,27 @@ func getOverallStatus() (o overallStatus) {
|
||||
if age := now.Sub(lastDERPMapAt); age > time.Minute {
|
||||
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
|
||||
}
|
||||
|
||||
addPairMeta := func(pair nodePair) {
|
||||
st, ok := state[pair]
|
||||
age := now.Sub(st.at).Round(time.Second)
|
||||
switch {
|
||||
case !ok:
|
||||
o.addBadf("no state for %v", pair)
|
||||
case st.err != nil:
|
||||
o.addBadf("%v: %v", pair, st.err)
|
||||
case age > 90*time.Second:
|
||||
o.addBadf("%v: update is %v old", pair, age)
|
||||
default:
|
||||
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
||||
}
|
||||
}
|
||||
|
||||
for _, reg := range sortedRegions(lastDERPMap) {
|
||||
for _, from := range reg.Nodes {
|
||||
addPairMeta(nodePair{"UDP", from.Name})
|
||||
for _, to := range reg.Nodes {
|
||||
pair := nodePair{from.Name, to.Name}
|
||||
st, ok := state[pair]
|
||||
age := now.Sub(st.at).Round(time.Second)
|
||||
switch {
|
||||
case !ok:
|
||||
o.addBadf("no state for %v", pair)
|
||||
case st.err != nil:
|
||||
o.addBadf("%v: %v", pair, st.err)
|
||||
case age > 90*time.Second:
|
||||
o.addBadf("%v: update is %v old", pair, age)
|
||||
default:
|
||||
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
||||
}
|
||||
addPairMeta(nodePair{from.Name, to.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +124,8 @@ func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
|
||||
}
|
||||
|
||||
type nodePair struct {
|
||||
from, to string // DERPNode.Name
|
||||
from string // DERPNode.Name, or "UDP" for a STUN query to 'to'
|
||||
to string // DERPNode.Name
|
||||
}
|
||||
|
||||
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
|
||||
@@ -177,6 +185,8 @@ func probe() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, from := range reg.Nodes {
|
||||
latency, err := probeUDP(ctx, dm, from)
|
||||
setState(nodePair{"UDP", from.Name}, latency, err)
|
||||
for _, to := range reg.Nodes {
|
||||
latency, err := probeNodePair(ctx, dm, from, to)
|
||||
setState(nodePair{from.Name, to.Name}, latency, err)
|
||||
@@ -189,6 +199,65 @@ func probe() error {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func probeUDP(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (latency time.Duration, err error) {
|
||||
pc, err := net.ListenPacket("udp", ":0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer pc.Close()
|
||||
uc := pc.(*net.UDPConn)
|
||||
|
||||
tx := stun.NewTxID()
|
||||
req := stun.Request(tx)
|
||||
|
||||
for _, ipStr := range []string{n.IPv4, n.IPv6} {
|
||||
if ipStr == "" {
|
||||
continue
|
||||
}
|
||||
port := n.STUNPort
|
||||
if port == -1 {
|
||||
continue
|
||||
}
|
||||
if port == 0 {
|
||||
port = 3478
|
||||
}
|
||||
for {
|
||||
ip := net.ParseIP(ipStr)
|
||||
_, err := uc.WriteToUDP(req, &net.UDPAddr{IP: ip, Port: port})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
buf := make([]byte, 1500)
|
||||
uc.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
t0 := time.Now()
|
||||
n, _, err := uc.ReadFromUDP(buf)
|
||||
d := time.Since(t0)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return 0, fmt.Errorf("timeout reading from %v: %v", ip, err)
|
||||
}
|
||||
if d < time.Second {
|
||||
return 0, fmt.Errorf("error reading from %v: %v", ip, err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
txBack, _, _, err := stun.ParseResponse(buf[:n])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing STUN response from %v: %v", ip, err)
|
||||
}
|
||||
if txBack != tx {
|
||||
return 0, fmt.Errorf("read wrong tx back from %v", ip)
|
||||
}
|
||||
if latency == 0 || d < latency {
|
||||
latency = d
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return latency, nil
|
||||
}
|
||||
|
||||
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
|
||||
// The passed in context is a minute for the whole region. The
|
||||
// idea is that each node pair in the region will be done
|
||||
@@ -275,7 +344,7 @@ func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.D
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
|
||||
priv := key.NewPrivate()
|
||||
priv := key.NewNode()
|
||||
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
|
||||
rid := n.RegionID
|
||||
return &tailcfg.DERPRegion{
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// microproxy proxies incoming HTTPS connections to another
|
||||
// destination. Instead of managing its own TLS certificates, it
|
||||
// borrows issued certificates and keys from an autocert directory.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":4430", "server address")
|
||||
certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
|
||||
hostname = flag.String("hostname", "", "hostname to serve")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
|
||||
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
|
||||
insecure = flag.Bool("insecure", false, "serve over http, for development")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *logCollection != "" {
|
||||
logpolicy.New(*logCollection)
|
||||
}
|
||||
|
||||
ne, err := url.Parse(*nodeExporter)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(ne)
|
||||
proxy.FlushInterval = time.Second
|
||||
|
||||
if _, err = url.Parse(*goVarsURL); err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux) // registers /debug/*
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
|
||||
Quiet200s: true,
|
||||
Logf: log.Printf,
|
||||
})))
|
||||
|
||||
ch := &certHolder{
|
||||
hostname: *hostname,
|
||||
path: filepath.Join(*certdir, *hostname),
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
if !*insecure {
|
||||
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = httpsrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type goVarsHandler struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
for k, i := range obj {
|
||||
if prefix != "" {
|
||||
k = prefix + "_" + k
|
||||
}
|
||||
switch v := i.(type) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
const saveConfigReject = "control_save_config_rejected_"
|
||||
const saveConfig = "control_save_config_"
|
||||
switch {
|
||||
case strings.HasPrefix(k, saveConfigReject):
|
||||
fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
|
||||
case strings.HasPrefix(k, saveConfig):
|
||||
fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
|
||||
default:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
resp, err := http.Get(h.url)
|
||||
if err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var mon map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
promPrint(w, "", mon)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certHolder loads and caches a TLS certificate from disk, reloading
|
||||
// it every hour.
|
||||
type certHolder struct {
|
||||
hostname string // only hostname allowed in SNI
|
||||
path string // path of certificate+key combined PEM file
|
||||
|
||||
mu sync.Mutex
|
||||
cert *tls.Certificate // cached parsed cert+key
|
||||
loaded time.Time
|
||||
}
|
||||
|
||||
func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ch.ServerName != c.hostname {
|
||||
return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if time.Since(c.loaded) > time.Hour {
|
||||
if err := c.loadLocked(); err != nil {
|
||||
log.Printf("Reloading cert %q: %v", c.path, err)
|
||||
// continue anyway, we might be able to serve off the stale cert.
|
||||
}
|
||||
}
|
||||
return c.cert, nil
|
||||
}
|
||||
|
||||
// load reloads the TLS certificate and key from disk. Caller must
|
||||
// hold mu.
|
||||
func (c *certHolder) loadLocked() error {
|
||||
bs, err := ioutil.ReadFile(c.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %v", c.path, err)
|
||||
}
|
||||
cert, err := tls.X509KeyPair(bs, bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %q: %v", c.path, err)
|
||||
}
|
||||
|
||||
c.cert = &cert
|
||||
c.loaded = time.Now()
|
||||
return nil
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/net/speedtest"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
@@ -33,6 +32,6 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(logMarker)
|
||||
outln(logMarker)
|
||||
return nil
|
||||
}
|
||||
|
||||
156
cmd/tailscale/cli/cert.go
Normal file
156
cmd/tailscale/cli/cert.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var certCmd = &ffcli.Command{
|
||||
Name: "cert",
|
||||
Exec: runCert,
|
||||
ShortHelp: "get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("cert")
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var certArgs struct {
|
||||
certFile string
|
||||
keyFile string
|
||||
serve bool
|
||||
}
|
||||
|
||||
func runCert(ctx context.Context, args []string) error {
|
||||
if certArgs.serve {
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
|
||||
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
|
||||
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
|
||||
}),
|
||||
}
|
||||
log.Printf("running TLS server on :443 ...")
|
||||
return s.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
var hint bytes.Buffer
|
||||
if st, err := tailscale.Status(ctx); err == nil {
|
||||
if st.BackendState != ipn.Running.String() {
|
||||
fmt.Fprintf(&hint, "\nTailscale is not running.\n")
|
||||
} else if len(st.CertDomains) == 0 {
|
||||
fmt.Fprintf(&hint, "\nHTTPS cert support is not enabled/configured for your tailnet.\n")
|
||||
} else if len(st.CertDomains) == 1 {
|
||||
fmt.Fprintf(&hint, "\nFor domain, use %q.\n", st.CertDomains[0])
|
||||
} else {
|
||||
fmt.Fprintf(&hint, "\nValid domain options: %q.\n", st.CertDomains)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Usage: tailscale cert [flags] <domain>%s", hint.Bytes())
|
||||
}
|
||||
domain := args[0]
|
||||
|
||||
printf := func(format string, a ...interface{}) {
|
||||
printf(format, a...)
|
||||
}
|
||||
if certArgs.certFile == "-" || certArgs.keyFile == "-" {
|
||||
printf = log.Printf
|
||||
log.SetFlags(0)
|
||||
}
|
||||
if certArgs.certFile == "" && certArgs.keyFile == "" {
|
||||
certArgs.certFile = domain + ".crt"
|
||||
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
|
||||
}
|
||||
needMacWarning := version.IsSandboxedMacOS()
|
||||
macWarn := func() {
|
||||
if !needMacWarning {
|
||||
return
|
||||
}
|
||||
needMacWarning = false
|
||||
dir := "io.tailscale.ipn.macos"
|
||||
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)
|
||||
}
|
||||
if certArgs.certFile != "" {
|
||||
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certArgs.certFile != "-" {
|
||||
macWarn()
|
||||
if certChanged {
|
||||
printf("Wrote public cert to %v\n", certArgs.certFile)
|
||||
} else {
|
||||
printf("Public cert unchanged at %v\n", certArgs.certFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
if certArgs.keyFile != "" {
|
||||
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certArgs.keyFile != "-" {
|
||||
macWarn()
|
||||
if keyChanged {
|
||||
printf("Wrote private key to %v\n", certArgs.keyFile)
|
||||
} else {
|
||||
printf("Private key unchanged at %v\n", certArgs.keyFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
|
||||
if filename == "-" {
|
||||
Stdout.Write(contents)
|
||||
return false, nil
|
||||
}
|
||||
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
|
||||
return false, nil
|
||||
}
|
||||
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -19,10 +19,11 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
@@ -30,6 +31,22 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
var Stderr io.Writer = os.Stderr
|
||||
var Stdout io.Writer = os.Stdout
|
||||
|
||||
func printf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(Stdout, format, a...)
|
||||
}
|
||||
|
||||
// outln is like fmt.Println in the common case, except when Stdout is
|
||||
// changed (as in js/wasm).
|
||||
//
|
||||
// It's not named println because that looks like the Go built-in
|
||||
// which goes to stderr and formats slightly differently.
|
||||
func outln(a ...interface{}) {
|
||||
fmt.Fprintln(Stdout, a...)
|
||||
}
|
||||
|
||||
// ActLikeCLI reports whether a GUI application should act like the
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
@@ -76,13 +93,30 @@ func ActLikeCLI() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func newFlagSet(name string) *flag.FlagSet {
|
||||
onError := flag.ExitOnError
|
||||
if runtime.GOOS == "js" {
|
||||
onError = flag.ContinueOnError
|
||||
}
|
||||
fs := flag.NewFlagSet(name, onError)
|
||||
fs.SetOutput(Stderr)
|
||||
return fs
|
||||
}
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) error {
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
var warnOnce sync.Once
|
||||
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
|
||||
warnOnce.Do(func() {
|
||||
fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
|
||||
})
|
||||
})
|
||||
|
||||
rootfs := newFlagSet("tailscale")
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
@@ -107,6 +141,7 @@ change in the future.
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
@@ -122,23 +157,33 @@ change in the future.
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
if Fatalf != nil {
|
||||
Fatalf(format, a...)
|
||||
return
|
||||
}
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
// Fatalf, if non-nil, is used instead of log.Fatalf.
|
||||
var Fatalf func(format string, a ...interface{})
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
@@ -607,10 +608,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
var warnBuf tstest.MemLogger
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
@@ -619,7 +617,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
got, err := prefsFromUpArgs(tt.args, warnBuf.Logf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
@@ -761,6 +759,18 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
wantErrSubtr: "can't change --login-server without --force-reauth",
|
||||
},
|
||||
{
|
||||
name: "change_tags",
|
||||
flags: []string{"--advertise-tags=tag:foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
@@ -27,20 +27,25 @@ var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs := newFlagSet("debug")
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
|
||||
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
|
||||
fs.BoolVar(&debugArgs.derpMap, "derp", false, "If true, dump DERP map")
|
||||
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.BoolVar(&debugArgs.env, "env", false, "dump environment")
|
||||
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
|
||||
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
env bool
|
||||
localCreds bool
|
||||
goroutines bool
|
||||
ipn bool
|
||||
@@ -49,35 +54,84 @@ var debugArgs struct {
|
||||
file string
|
||||
prefs bool
|
||||
pretty bool
|
||||
cpuSec int
|
||||
cpuFile string
|
||||
memFile string
|
||||
}
|
||||
|
||||
func writeProfile(dst string, v []byte) error {
|
||||
if dst == "-" {
|
||||
_, err := Stdout.Write(v)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, v, 0600)
|
||||
}
|
||||
|
||||
func outName(dst string) string {
|
||||
if dst == "-" {
|
||||
return "stdout"
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
return fmt.Sprintf("%s (warning: sandboxed macOS binaries write to Library/Containers; use - to write to stdout and redirect to file instead)", dst)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.env {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if debugArgs.localCreds {
|
||||
port, token, err := safesocket.LocalTCPPortAndToken()
|
||||
if err == nil {
|
||||
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
|
||||
printf("curl http://localhost:41112/localapi/v0/status\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
if out := debugArgs.cpuFile; out != "" {
|
||||
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
|
||||
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := writeProfile(out, v); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("CPU profile written to %s", outName(out))
|
||||
}
|
||||
}
|
||||
if out := debugArgs.memFile; out != "" {
|
||||
log.Printf("Capturing memory profile ...")
|
||||
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := writeProfile(out, v); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Memory profile written to %s", outName(out))
|
||||
}
|
||||
}
|
||||
if debugArgs.prefs {
|
||||
prefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugArgs.pretty {
|
||||
fmt.Println(prefs.Pretty())
|
||||
outln(prefs.Pretty())
|
||||
} else {
|
||||
j, _ := json.MarshalIndent(prefs, "", "\t")
|
||||
fmt.Println(string(j))
|
||||
outln(string(j))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -86,7 +140,7 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.derpMap {
|
||||
@@ -96,7 +150,7 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
|
||||
)
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(dm)
|
||||
return nil
|
||||
@@ -110,7 +164,7 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
@@ -120,9 +174,9 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
if debugArgs.file == "get" {
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
fatalf("%v\n", err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e := json.NewEncoder(Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
@@ -136,7 +190,7 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
log.Printf("Size: %v\n", size)
|
||||
io.Copy(os.Stdout, rc)
|
||||
io.Copy(Stdout, rc)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || windows || darwin
|
||||
// +build linux windows darwin
|
||||
|
||||
package cli
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !linux && !windows && !darwin
|
||||
// +build !linux,!windows,!darwin
|
||||
|
||||
package cli
|
||||
|
||||
@@ -7,10 +7,8 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
@@ -25,7 +23,7 @@ var downCmd = &ffcli.Command{
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
@@ -33,7 +31,7 @@ func runDown(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
}
|
||||
if st.BackendState == "Stopped" {
|
||||
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
|
||||
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
|
||||
return nil
|
||||
}
|
||||
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -55,7 +55,7 @@ var fileCpCmd = &ffcli.Command{
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cp", flag.ExitOnError)
|
||||
fs := newFlagSet("cp")
|
||||
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
|
||||
@@ -91,7 +91,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
} else if hadBrackets && (err != nil || !ip.Is6()) {
|
||||
return errors.New("unexpected brackets around target")
|
||||
}
|
||||
ip, err := tailscaleIPFromArg(ctx, target)
|
||||
ip, _, err := tailscaleIPFromArg(ctx, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
if isOffline {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
|
||||
fmt.Fprintf(Stderr, "# warning: %s is offline\n", target)
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
@@ -172,7 +172,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
res.Body.Close()
|
||||
continue
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
io.Copy(Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func runCpTargets(ctx context.Context, args []string) error {
|
||||
if detail != "" {
|
||||
detail = "\t" + detail
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -304,7 +304,7 @@ var fileGetCmd = &ffcli.Command{
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("get", flag.ExitOnError)
|
||||
fs := newFlagSet("get")
|
||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
|
||||
return fs
|
||||
@@ -415,7 +415,7 @@ func waitForFile(ctx context.Context) error {
|
||||
fileWaiting := make(chan bool, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
fatalf("Notify.ErrMessage: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -23,7 +23,7 @@ var ipCmd = &ffcli.Command{
|
||||
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
|
||||
Exec: runIP,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ip", flag.ExitOnError)
|
||||
fs := newFlagSet("ip")
|
||||
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
|
||||
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
|
||||
return fs
|
||||
@@ -46,7 +46,7 @@ func runIP(ctx context.Context, args []string) error {
|
||||
|
||||
v4, v6 := ipArgs.want4, ipArgs.want6
|
||||
if v4 && v6 {
|
||||
return errors.New("tailscale up -4 and -6 are mutually exclusive")
|
||||
return errors.New("tailscale ip -4 and -6 are mutually exclusive")
|
||||
}
|
||||
if !v4 && !v6 {
|
||||
v4, v6 = true, true
|
||||
@@ -57,7 +57,7 @@ func runIP(ctx context.Context, args []string) error {
|
||||
}
|
||||
ips := st.TailscaleIPs
|
||||
if of != "" {
|
||||
ip, err := tailscaleIPFromArg(ctx, of)
|
||||
ip, _, err := tailscaleIPFromArg(ctx, of)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func runIP(ctx context.Context, args []string) error {
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
match = true
|
||||
fmt.Println(ip)
|
||||
outln(ip)
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
@@ -101,5 +101,12 @@ func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus,
|
||||
}
|
||||
}
|
||||
}
|
||||
if ps := st.Self; ps != nil {
|
||||
for _, pip := range ps.TailscaleIPs {
|
||||
if ip == pip {
|
||||
return ps, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ a reauthentication.
|
||||
|
||||
func runLogout(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
return tailscale.Logout(ctx)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netcheck"
|
||||
@@ -33,7 +33,7 @@ var netcheckCmd = &ffcli.Command{
|
||||
ShortHelp: "Print an analysis of local network conditions",
|
||||
Exec: runNetcheck,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
|
||||
fs := newFlagSet("netcheck")
|
||||
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
|
||||
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
|
||||
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
|
||||
@@ -60,11 +60,15 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(netcheckArgs.format, "json") {
|
||||
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
}
|
||||
|
||||
dm, err := tailscale.CurrentDERPMap(ctx)
|
||||
if err != nil {
|
||||
noRegions := dm != nil && len(dm.Regions) == 0
|
||||
if noRegions {
|
||||
log.Printf("No DERP map from tailscaled; using default.")
|
||||
}
|
||||
if err != nil || noRegions {
|
||||
dm, err = prodDERPMap(ctx, http.DefaultClient)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -78,7 +82,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("netcheck: %v", err)
|
||||
return fmt.Errorf("netcheck: %w", err)
|
||||
}
|
||||
if err := printReport(dm, report); err != nil {
|
||||
return err
|
||||
@@ -108,36 +112,36 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
}
|
||||
if j != nil {
|
||||
j = append(j, '\n')
|
||||
os.Stdout.Write(j)
|
||||
Stdout.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nReport:\n")
|
||||
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
||||
printf("\nReport:\n")
|
||||
printf("\t* UDP: %v\n", report.UDP)
|
||||
if report.GlobalV4 != "" {
|
||||
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
} else {
|
||||
fmt.Printf("\t* IPv4: (no addr found)\n")
|
||||
printf("\t* IPv4: (no addr found)\n")
|
||||
}
|
||||
if report.GlobalV6 != "" {
|
||||
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
fmt.Printf("\t* IPv6: (no addr found)\n")
|
||||
printf("\t* IPv6: (no addr found)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* IPv6: no\n")
|
||||
printf("\t* IPv6: no\n")
|
||||
}
|
||||
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
fmt.Printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
|
||||
// When DERP latency checking failed,
|
||||
// magicsock will try to pick the DERP server that
|
||||
// most of your other nodes are also using
|
||||
if len(report.RegionLatency) == 0 {
|
||||
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
@@ -164,7 +168,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
if netcheckArgs.verbose {
|
||||
derpNum = fmt.Sprintf("derp%d, ", rid)
|
||||
}
|
||||
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -45,7 +45,7 @@ relay node.
|
||||
`),
|
||||
Exec: runPing,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs := newFlagSet("ping")
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
|
||||
@@ -74,7 +74,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
fatalf("Notify.ErrMessage: %v", *n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
@@ -84,10 +84,14 @@ func runPing(ctx context.Context, args []string) error {
|
||||
go func() { pumpErr <- pump(ctx, bc, c) }()
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self {
|
||||
printf("%v is local Tailscale IP\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
@@ -101,12 +105,16 @@ func runPing(ctx context.Context, args []string) error {
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
printf("timeout waiting for ping reply\n")
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
if pr.IsLocalIP {
|
||||
outln(pr.Err)
|
||||
return nil
|
||||
}
|
||||
return errors.New(pr.Err)
|
||||
}
|
||||
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
|
||||
@@ -124,7 +132,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr.PeerAPIPort != 0 {
|
||||
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
|
||||
}
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
@@ -147,33 +155,39 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, self bool, err error) {
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
return hostOrIP, nil
|
||||
return hostOrIP, false, nil
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", false, err
|
||||
}
|
||||
match := func(ps *ipnstate.PeerStatus) bool {
|
||||
return strings.EqualFold(hostOrIP, dnsOrQuoteHostname(st, ps)) || hostOrIP == ps.DNSName
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
if match(ps) {
|
||||
if len(ps.TailscaleIPs) == 0 {
|
||||
return "", errors.New("node found but lacks an IP")
|
||||
return "", false, errors.New("node found but lacks an IP")
|
||||
}
|
||||
return ps.TailscaleIPs[0].String(), nil
|
||||
return ps.TailscaleIPs[0].String(), false, nil
|
||||
}
|
||||
}
|
||||
if match(st.Self) && len(st.Self.TailscaleIPs) > 0 {
|
||||
return st.Self.TailscaleIPs[0].String(), true, nil
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return "", fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
return "", false, fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return "", fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
return "", false, fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
return addrs[0], nil
|
||||
return addrs[0], false, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -33,7 +31,7 @@ var statusCmd = &ffcli.Command{
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
@@ -63,7 +61,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if statusArgs.json {
|
||||
if statusArgs.active {
|
||||
for peer, ps := range st.Peer {
|
||||
if !peerActive(ps) {
|
||||
if !ps.Active {
|
||||
delete(st.Peer, peer)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +70,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s", j)
|
||||
printf("%s", j)
|
||||
return nil
|
||||
}
|
||||
if statusArgs.web {
|
||||
@@ -81,7 +79,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
statusURL := interfaces.HTTPOfListener(ln)
|
||||
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
@@ -110,28 +108,35 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
os.Exit(1)
|
||||
case ipn.Stopped.String():
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
outln("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
fmt.Println("Logged out.")
|
||||
outln("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
|
||||
printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
fmt.Println("Machine is not yet authorized by tailnet admin.")
|
||||
outln("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String():
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
if len(st.Health) > 0 {
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
printf("# - %s\n", m)
|
||||
}
|
||||
outln()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
firstIPString(ps.TailscaleIPs),
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
@@ -140,7 +145,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
@@ -179,24 +184,16 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
if statusArgs.active && !ps.Active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
// peerActive reports whether ps has recent activity.
|
||||
//
|
||||
// TODO: have the server report this bool instead.
|
||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
return !ps.LastWrite.IsZero() && mono.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -17,7 +18,8 @@ import (
|
||||
"sync"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
@@ -61,8 +63,9 @@ func effectiveGOOS() string {
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
upf := newFlagSet("up")
|
||||
|
||||
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
@@ -74,7 +77,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.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")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
@@ -99,6 +102,7 @@ func defaultNetfilterMode() string {
|
||||
}
|
||||
|
||||
type upArgsT struct {
|
||||
qr bool
|
||||
reset bool
|
||||
server string
|
||||
acceptRoutes bool
|
||||
@@ -114,15 +118,28 @@ type upArgsT struct {
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
hostname string
|
||||
opUser string
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
v := a.authKeyOrFile
|
||||
if strings.HasPrefix(v, "file:") {
|
||||
file := strings.TrimPrefix(v, "file:")
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -130,17 +147,11 @@ var (
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
if advertiseRoutes != "" {
|
||||
var default4, default6 bool
|
||||
advroutes := strings.Split(advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
@@ -162,7 +173,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
if advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
@@ -176,6 +187,20 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
}
|
||||
return routes[i].IP().Less(routes[j].IP())
|
||||
})
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
routes, err := calcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
@@ -252,7 +277,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
// 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
|
||||
// change the prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc).
|
||||
// advertised routes, etc).
|
||||
//
|
||||
// It returns simpleUp if we're running a simple "tailscale up" to
|
||||
// transition to running from a previously-logged-in but down state,
|
||||
@@ -272,6 +297,8 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
@@ -280,8 +307,9 @@ 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.authKey == "" &&
|
||||
!controlURLChanged
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
@@ -308,7 +336,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
if upArgs.authKey != "" {
|
||||
if upArgs.authKeyOrFile != "" {
|
||||
// Issue 1755: when using an authkey, don't
|
||||
// show an authURL that might still be pending
|
||||
// from a previous non-completed interactive
|
||||
@@ -372,8 +400,8 @@ func runUp(ctx context.Context, args []string) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
startingOrRunning := make(chan bool, 1) // gets value once starting or running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
running := make(chan bool, 1) // gets value once in state ipn.Running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
@@ -407,15 +435,15 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
case ipn.Starting, ipn.Running:
|
||||
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 {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case startingOrRunning <- true:
|
||||
case running <- true:
|
||||
default:
|
||||
}
|
||||
cancel()
|
||||
@@ -423,7 +451,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
@@ -452,9 +489,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
authKey, err := upArgs.getAuthKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
AuthKey: authKey,
|
||||
UpdatePrefs: prefs,
|
||||
}
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
@@ -478,17 +519,29 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// This whole 'up' mechanism is too complicated and results in
|
||||
// hairy stuff like this select. We're ultimately waiting for
|
||||
// 'running' to be done, but even in the case where
|
||||
// it succeeds, other parts may shut down concurrently so we
|
||||
// need to prioritize reads from 'running' if it's
|
||||
// readable; its send does happen before the pump mechanism
|
||||
// shuts down. (Issue 2333)
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
case <-running:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
case <-running:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -535,7 +588,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
case "authkey", "force-reauth", "reset", "qr":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -20,7 +19,7 @@ var versionCmd = &ffcli.Command{
|
||||
ShortUsage: "version [flags]",
|
||||
ShortHelp: "Print Tailscale version",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("version", flag.ExitOnError)
|
||||
fs := newFlagSet("version")
|
||||
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
|
||||
return fs
|
||||
})(),
|
||||
@@ -33,19 +32,19 @@ var versionArgs struct {
|
||||
|
||||
func runVersion(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
if !versionArgs.daemon {
|
||||
fmt.Println(version.String())
|
||||
outln(version.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
printf("Client: %s\n", version.String())
|
||||
|
||||
st, err := tailscale.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Daemon: %s\n", st.Version)
|
||||
printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1335,3 +1335,14 @@ html {
|
||||
border-color: #6c94ec;
|
||||
border-color: rgba(108, 148, 236, var(--border-opacity));
|
||||
}
|
||||
|
||||
.button-red {
|
||||
background-color: #d04841;
|
||||
border-color: #d04841;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.button-red:enabled:hover {
|
||||
background-color: #b22d30;
|
||||
border-color: #b22d30;
|
||||
}
|
||||
|
||||
@@ -7,23 +7,27 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -49,11 +53,13 @@ func init() {
|
||||
}
|
||||
|
||||
type tmplData struct {
|
||||
Profile tailcfg.UserProfile
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
Profile tailcfg.UserProfile
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
AdvertiseExitNode bool
|
||||
AdvertiseRoutes string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -70,7 +76,7 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
`),
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
webf := newFlagSet("web")
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
return webf
|
||||
@@ -83,9 +89,32 @@ var webArgs struct {
|
||||
cgi bool
|
||||
}
|
||||
|
||||
func tlsConfigFromEnvironment() *tls.Config {
|
||||
crt := os.Getenv("TLS_CRT_PEM")
|
||||
key := os.Getenv("TLS_KEY_PEM")
|
||||
if crt == "" || key == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We support passing in the complete certificate and key from environment
|
||||
// variables because pfSense stores its cert+key in the PHP config. We populate
|
||||
// TLS_CRT_PEM and TLS_KEY_PEM from PHP code before starting tailscale web.
|
||||
// These are the PEM-encoded Certificate and Private Key.
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(crt), []byte(key))
|
||||
if err != nil {
|
||||
log.Printf("tlsConfigFromEnvironment: %v", err)
|
||||
|
||||
// Fallback to unencrypted HTTP.
|
||||
return nil
|
||||
}
|
||||
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
@@ -96,8 +125,20 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
tlsConfig := tlsConfigFromEnvironment()
|
||||
if tlsConfig != nil {
|
||||
server := &http.Server{
|
||||
Addr: webArgs.listen,
|
||||
TLSConfig: tlsConfig,
|
||||
Handler: http.HandlerFunc(webHandler),
|
||||
}
|
||||
|
||||
log.Printf("web server runNIng on: https://%s", server.Addr)
|
||||
return server.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
}
|
||||
|
||||
// urlOfListenAddr parses a given listen address into a formatted URL
|
||||
@@ -266,15 +307,45 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
var postData struct {
|
||||
AdvertiseRoutes string
|
||||
AdvertiseExitNode bool
|
||||
Reauthenticate bool
|
||||
}
|
||||
type mi map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
prefs, err := tailscale.GetPrefs(r.Context())
|
||||
if err != nil && !postData.Reauthenticate {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
prefs.AdvertiseRoutes = routes
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
if url != "" {
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
} else {
|
||||
io.WriteString(w, "{}")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -283,6 +354,11 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
prefs, err := tailscale.GetPrefs(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
profile := st.User[st.Self.UserID]
|
||||
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
||||
@@ -292,6 +368,18 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Status: st.BackendState,
|
||||
DeviceName: deviceName,
|
||||
}
|
||||
exitNodeRouteV4 := netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netaddr.MustParseIPPrefix("::/0")
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
||||
data.AdvertiseExitNode = true
|
||||
} else {
|
||||
if data.AdvertiseRoutes != "" {
|
||||
data.AdvertiseRoutes = ","
|
||||
}
|
||||
data.AdvertiseRoutes += r.String()
|
||||
}
|
||||
}
|
||||
if len(st.TailscaleIPs) != 0 {
|
||||
data.IP = st.TailscaleIPs[0].String()
|
||||
}
|
||||
@@ -305,13 +393,15 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authURL string, retErr error) {
|
||||
if prefs == nil {
|
||||
prefs = ipn.NewPrefs()
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
@@ -358,6 +448,14 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
if !forceReauth && n.Prefs != nil {
|
||||
p1, p2 := *n.Prefs, *prefs
|
||||
p1.Persist = nil
|
||||
p2.Persist = nil
|
||||
if p1.Equals(&p2) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
// we're subscribed to updates. Otherwise we can miss
|
||||
@@ -375,10 +473,15 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
bc.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
})
|
||||
bc.StartLoginInteractive()
|
||||
if forceReauth {
|
||||
bc.StartLoginInteractive()
|
||||
}
|
||||
|
||||
<-pumpCtx.Done() // wait for authURL or complete failure
|
||||
if authURL == "" && retErr == nil {
|
||||
if !forceReauth {
|
||||
return "", nil // no auth URL is fine
|
||||
}
|
||||
retErr = pumpCtx.Err()
|
||||
}
|
||||
if authURL == "" && retErr == nil {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<h4 class="truncate leading-normal">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -86,14 +86,30 @@
|
||||
<div class="mb-4">
|
||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
<div class="mb-4">
|
||||
<a href="#" class="mb-4 js-advertiseExitNode">
|
||||
{{if .AdvertiseExitNode}}
|
||||
<button class="button button-red button-medium" id="enabled">Stop advertising Exit Node</button>
|
||||
{{else}}
|
||||
<button class="button button-blue button-medium" id="enabled">Advertise as Exit Node</button>
|
||||
{{end}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
<script>(function () {
|
||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
||||
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||
let fetchingUrl = false;
|
||||
var data = {
|
||||
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
|
||||
AdvertiseExitNode: advertiseExitNode,
|
||||
Reauthenticate: false
|
||||
};
|
||||
|
||||
function handleClick(e) {
|
||||
function postData(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (fetchingUrl) {
|
||||
@@ -116,7 +132,8 @@ function handleClick(e) {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
@@ -134,9 +151,19 @@ function handleClick(e) {
|
||||
});
|
||||
}
|
||||
|
||||
Array.from(loginButtons).forEach(el => {
|
||||
el.addEventListener("click", handleClick);
|
||||
Array.from(document.querySelectorAll(".js-loginButton")).forEach(el => {
|
||||
el.addEventListener("click", function(e) {
|
||||
data.Reauthenticate = true;
|
||||
postData(e);
|
||||
});
|
||||
})
|
||||
Array.from(document.querySelectorAll(".js-advertiseExitNode")).forEach(el => {
|
||||
el.addEventListener("click", function(e) {
|
||||
data.AdvertiseExitNode = !advertiseExitNode;
|
||||
postData(e);
|
||||
});
|
||||
})
|
||||
|
||||
})();</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
@@ -20,23 +24,29 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
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/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
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
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
@@ -44,23 +54,24 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
@@ -70,14 +81,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/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/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/http/httpguts from net/http+
|
||||
@@ -101,7 +112,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
compress/zlib from image/png
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -124,11 +135,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -139,14 +146,17 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
flag from github.com/peterbourgon/ff/v3+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash from crypto+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
html/template from tailscale.com/cmd/tailscale/cli
|
||||
image from github.com/skip2/go-qrcode+
|
||||
image/color from github.com/skip2/go-qrcode+
|
||||
image/png from github.com/skip2/go-qrcode
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
@@ -169,10 +179,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember
|
||||
path from debug/dwarf+
|
||||
path from html/template+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
@@ -181,7 +191,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
ifconfig bool // print network state once and exit
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
@@ -45,6 +46,7 @@ var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
@@ -59,8 +61,11 @@ func debugMode(args []string) error {
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.ifconfig {
|
||||
return runMonitor(ctx, false)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
return runMonitor(ctx, true)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
@@ -71,7 +76,7 @@ func debugMode(args []string) error {
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
func runMonitor(ctx context.Context, loop bool) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
@@ -88,8 +93,13 @@ func runMonitor(ctx context.Context) error {
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
if loop {
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
}
|
||||
dump(mon.InterfaceState())
|
||||
if !loop {
|
||||
return nil
|
||||
}
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
@@ -128,11 +138,18 @@ func getURL(ctx context.Context, urlStr string) error {
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) got: auth of %d bytes, err=%v", proxyURL, len(auth), err)
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
if auth != "" {
|
||||
// We used log.Printf above (for timestamps).
|
||||
// Use fmt.Printf here instead just to appease
|
||||
// a security scanner, despite log.Printf only
|
||||
// going to stdout.
|
||||
fmt.Printf("... Proxy-Authorization = %q\n", auth)
|
||||
}
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
@@ -176,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
@@ -206,6 +223,22 @@ func debugPortmap(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "pcp":
|
||||
portmapper.DisablePMP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "upnp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisablePMP = true
|
||||
default:
|
||||
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
|
||||
}
|
||||
|
||||
done := make(chan bool, 1)
|
||||
|
||||
var c *portmapper.Client
|
||||
@@ -248,6 +281,13 @@ func debugPortmap(ctx context.Context) error {
|
||||
}
|
||||
logf("gw=%v; self=%v", gw, selfIP)
|
||||
|
||||
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uc.Close()
|
||||
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
|
||||
|
||||
res, err := c.Probe(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Probe: %v", err)
|
||||
@@ -259,13 +299,6 @@ func debugPortmap(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uc.Close()
|
||||
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("mapping: %v", ext)
|
||||
} else {
|
||||
|
||||
@@ -1,37 +1,106 @@
|
||||
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W 💣 github.com/alexbrainman/sspi 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/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
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/ratelimit from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||
L github.com/aws/aws-sdk-go-v2/aws/retry from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4 from github.com/aws/aws-sdk-go-v2/aws/signer/v4
|
||||
L github.com/aws/aws-sdk-go-v2/aws/signer/v4 from github.com/aws/aws-sdk-go-v2/service/internal/presigned-url+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/transport/http from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/config from tailscale.com/ipn/store/aws
|
||||
L github.com/aws/aws-sdk-go-v2/credentials from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/processcreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/ssocreds from github.com/aws/aws-sdk-go-v2/config
|
||||
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/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+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
|
||||
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||
L github.com/aws/aws-sdk-go-v2/service/internal/presigned-url from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssm from tailscale.com/ipn/store/aws
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssm/types from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
|
||||
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
|
||||
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
|
||||
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
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
|
||||
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
||||
github.com/go-multierror/multierror from tailscale.com/cmd/tailscaled+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
L 💣 github.com/mdlayher/netlink from tailscale.com/wgengine/monitor+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
L 💣 github.com/vishvananda/netlink from tailscale.com/wgengine/router
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/vishvananda/netlink
|
||||
L github.com/vishvananda/netns from github.com/vishvananda/netlink+
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
|
||||
@@ -41,11 +110,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
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+
|
||||
@@ -53,7 +122,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/tcpip+
|
||||
💣 inet.af/netstack/state 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+
|
||||
@@ -66,7 +135,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/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+
|
||||
@@ -80,104 +149,115 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
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/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
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/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
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/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/wgengine+
|
||||
💣 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+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
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/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/netknob from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/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/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/tshttpproxy from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/pad32 from tailscale.com/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+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
|
||||
tailscale.com/version from tailscale.com/client/tailscale+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor 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
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
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+
|
||||
@@ -191,13 +271,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/errgroup from github.com/tailscale/goupnp/httpu+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
@@ -209,7 +289,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -230,13 +309,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -245,20 +320,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/fnv from inet.af/netstack/tcpip/network/ipv6+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
@@ -270,19 +344,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/httputil from tailscale.com/ipn/localapi
|
||||
net/http/httputil from github.com/aws/smithy-go/transport/http+
|
||||
net/http/internal from net/http+
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled+
|
||||
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
@@ -296,5 +370,5 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
text/tabwriter from runtime/pprof
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
|
||||
79
cmd/tailscaled/proxy.go
Normal file
79
cmd/tailscaled/proxy.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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.
|
||||
|
||||
// HTTP proxy code
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// httpProxyHandler returns an HTTP proxy http.Handler using the
|
||||
// provided backend dialer.
|
||||
func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler {
|
||||
rp := &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {}, // no change
|
||||
Transport: &http.Transport{
|
||||
DialContext: dialer,
|
||||
},
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "CONNECT" {
|
||||
backURL := r.RequestURI
|
||||
if strings.HasPrefix(backURL, "/") || backURL == "*" {
|
||||
http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400)
|
||||
return
|
||||
}
|
||||
rp.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// CONNECT support:
|
||||
|
||||
dst := r.RequestURI
|
||||
c, err := dialer(r.Context(), "tcp", dst)
|
||||
if err != nil {
|
||||
w.Header().Set("Tailscale-Connect-Error", err.Error())
|
||||
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")
|
||||
|
||||
var clientSrc io.Reader = ccbuf
|
||||
if ccbuf.Reader.Buffered() == 0 {
|
||||
// In the common case (with no
|
||||
// buffered data), read directly from
|
||||
// the underlying client connection to
|
||||
// save some memory, letting the
|
||||
// bufio.Reader/Writer get GC'ed.
|
||||
clientSrc = cc
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(cc, c)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(c, clientSrc)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
})
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
@@ -68,19 +69,26 @@ func defaultTunName() string {
|
||||
}
|
||||
|
||||
var args struct {
|
||||
cleanup bool
|
||||
debug string
|
||||
tunname string // tun name, "userspace-networking", or comma-separated list thereof
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
||||
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
||||
// or comma-separated list thereof.
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
birdSocketPath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
httpProxyAddr string // listen address for HTTP proxy server
|
||||
}
|
||||
|
||||
var (
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
||||
createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms
|
||||
)
|
||||
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
@@ -103,10 +111,12 @@ func main() {
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
@@ -138,7 +148,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
}
|
||||
@@ -148,6 +158,11 @@ func main() {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
if args.birdSocketPath != "" && createBIRDClient == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
@@ -159,6 +174,34 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func trySynologyMigration(p string) error {
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return nil
|
||||
}
|
||||
|
||||
fi, err := os.Stat(p)
|
||||
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// File is empty or doesn't exist, try reading from the old path.
|
||||
|
||||
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
|
||||
if _, err := os.Stat(oldPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(oldPath, p); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -221,6 +264,9 @@ func run() error {
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
if err := trySynologyMigration(args.statepath); err != nil {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
@@ -234,19 +280,8 @@ func run() error {
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
var socksListener net.Listener
|
||||
if args.socksAddr != "" {
|
||||
var err error
|
||||
socksListener, err = net.Listen("tcp", args.socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(args.socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
|
||||
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
if err != nil {
|
||||
@@ -260,11 +295,19 @@ func run() error {
|
||||
ns = mustStartNetstack(logf, e, onlySubnets)
|
||||
}
|
||||
|
||||
if socksListener != nil {
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
|
||||
go func() {
|
||||
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
||||
}()
|
||||
}
|
||||
if socksListener != nil {
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
@@ -331,9 +374,9 @@ func shouldWrapNetstack() bool {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
case "windows", "darwin", "freebsd":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients).
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -344,7 +387,17 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
}
|
||||
|
||||
useNetstack = name == "userspace-networking"
|
||||
netns.SetEnabled(!useNetstack)
|
||||
|
||||
if args.birdSocketPath != "" && createBIRDClient != nil {
|
||||
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
@@ -352,6 +405,12 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
return nil, false, err
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
conf.IsTAP = true
|
||||
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
return e, false, err
|
||||
}
|
||||
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
@@ -408,3 +467,19 @@ func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *n
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func mustStartTCPListener(name, addr string) net.Listener {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("%v listener: %v", name, err)
|
||||
}
|
||||
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())
|
||||
}
|
||||
return ln
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Restart=on-failure
|
||||
RuntimeDirectory=tailscale
|
||||
RuntimeDirectoryMode=0755
|
||||
StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
StateDirectoryMode=0700
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
19
cmd/tailscaled/tailscaled_bird.go
Normal file
19
cmd/tailscaled/tailscaled_bird.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || darwin || freebsd || openbsd
|
||||
// +build linux darwin freebsd openbsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"tailscale.com/chirp"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) {
|
||||
return chirp.New(ctlSocket)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -63,6 +64,11 @@ type ipnService struct {
|
||||
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
svcAccepts := svc.AcceptStop
|
||||
if winutil.GetRegInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
svcAccepts |= svc.AcceptSessionChange
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
@@ -71,7 +77,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
ipnserver.BabysitProc(ctx, args, log.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
select {
|
||||
@@ -82,6 +88,9 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
cancel()
|
||||
case svc.Interrogate:
|
||||
changes <- cmd.CurrentStatus
|
||||
case svc.SessionChange:
|
||||
handleSessionChange(cmd)
|
||||
changes <- cmd.CurrentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,6 +273,20 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func handleSessionChange(chgRequest svc.ChangeRequest) {
|
||||
if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
|
||||
go func() {
|
||||
err := dns.Flush()
|
||||
if err != nil {
|
||||
log.Printf("Error flushing DNS on session unlock: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
|
||||
|
||||
98
cmd/testcontrol/testcontrol.go
Normal file
98
cmd/testcontrol/testcontrol.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Program testcontrol runs a simple test control server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var t fakeTB
|
||||
derpMap := integration.RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
|
||||
|
||||
control := &testcontrol.Server{
|
||||
DERPMap: derpMap,
|
||||
ExplicitBaseURL: "http://127.0.0.1:9911",
|
||||
}
|
||||
for i := 0; i < *flagNFake; i++ {
|
||||
control.AddFakeNode()
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", control)
|
||||
addr := "127.0.0.1:9911"
|
||||
log.Printf("listening on %s", addr)
|
||||
err := http.ListenAndServe(addr, mux)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type fakeTB struct {
|
||||
*testing.T
|
||||
}
|
||||
|
||||
func (t fakeTB) Cleanup(_ func()) {}
|
||||
func (t fakeTB) Error(args ...interface{}) {
|
||||
t.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Errorf(format string, args ...interface{}) {
|
||||
t.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Fail() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) FailNow() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) Failed() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) Fatal(args ...interface{}) {
|
||||
log.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Fatalf(format string, args ...interface{}) {
|
||||
log.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Helper() {}
|
||||
func (t fakeTB) Log(args ...interface{}) {
|
||||
log.Print(args...)
|
||||
}
|
||||
func (t fakeTB) Logf(format string, args ...interface{}) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Name() string {
|
||||
return "faketest"
|
||||
}
|
||||
func (t fakeTB) Setenv(key string, value string) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (t fakeTB) Skip(args ...interface{}) {
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) SkipNow() {
|
||||
t.Fatal("skipnow")
|
||||
}
|
||||
func (t fakeTB) Skipf(format string, args ...interface{}) {
|
||||
t.Logf(format, args...)
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) Skipped() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) TempDir() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// The tsshd binary is an SSH server that accepts connections
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
type LoginGoal struct {
|
||||
@@ -281,7 +281,6 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
@@ -431,7 +430,7 @@ func (c *Auto) mapRoutine() {
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %w", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
@@ -599,9 +598,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
State: state,
|
||||
}
|
||||
if err != nil {
|
||||
new.Err = err.Error()
|
||||
Err: err,
|
||||
}
|
||||
if statusFunc != nil {
|
||||
statusFunc(new)
|
||||
@@ -702,7 +699,7 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
|
||||
func (c *Auto) TestOnlyNodePublicKey() key.NodePublic {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type LoginFlags int
|
||||
const (
|
||||
LoginDefault = LoginFlags(0)
|
||||
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
|
||||
LoginEphemeral // set RegisterRequest.Ephemeral
|
||||
)
|
||||
|
||||
// Client represents a client connection to the control server.
|
||||
@@ -69,7 +70,7 @@ type Client interface {
|
||||
SetNetInfo(*tailcfg.NetInfo)
|
||||
// UpdateEndpoints changes the Endpoint structure that will be sent
|
||||
// in subsequent node registration requests.
|
||||
// TODO: localPort seems to be obsolete, remove it.
|
||||
// The localPort field is unused except for integration tests in another repo.
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
|
||||
@@ -75,10 +75,3 @@ func TestStatusEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package controlclient
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -29,10 +27,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -42,14 +41,12 @@ import (
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -64,33 +61,33 @@ type Direct struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
getMachinePrivKey func() (wgkey.Private, error)
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgkey.Key
|
||||
serverKey key.MachinePublic
|
||||
persist persist.Persist
|
||||
authKey string
|
||||
tryingNewKey wgkey.Private
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry *time.Time
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
endpoints []tailcfg.Endpoint
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
localPort uint16 // or zero to mean auto
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppresion
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (wgkey.Private, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
TimeNow func() time.Time // time.Now implementation used by Client
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
TimeNow func() time.Time // time.Now implementation used by Client
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey tailcfg.DiscoKey
|
||||
NewDecompressor func() (Decompressor, error)
|
||||
KeepAlive bool
|
||||
@@ -184,53 +181,13 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
pinger: opts.Pinger,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
} else {
|
||||
c.SetHostinfo(opts.Hostinfo)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
hostname = dnsname.FirstLabel(hostname)
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
func packageType() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
|
||||
return "choco"
|
||||
}
|
||||
case "darwin":
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
case "linux":
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
|
||||
return "snap"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetHostinfo clones the provided Hostinfo and remembers it for the
|
||||
// next update. It reports whether the Hostinfo has changed.
|
||||
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
@@ -362,14 +319,14 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if err != nil {
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
c.logf("control server key %s from %s", serverKey.HexString(), c.serverURL)
|
||||
c.logf("control server key %s from %s", serverKey.ShortString(), c.serverURL)
|
||||
|
||||
c.mu.Lock()
|
||||
c.serverKey = serverKey
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var oldNodeKey wgkey.Key
|
||||
var oldNodeKey key.NodePublic
|
||||
switch {
|
||||
case opt.Logout:
|
||||
tryingNewKey = persist.PrivateNodeKey
|
||||
@@ -378,12 +335,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
case regen || persist.PrivateNodeKey.IsZero():
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
c.logf("login keygen: %v", err)
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
tryingNewKey = key
|
||||
tryingNewKey = key.NewNode()
|
||||
default:
|
||||
// Try refreshing the current key first
|
||||
tryingNewKey = persist.PrivateNodeKey
|
||||
@@ -405,11 +357,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
now := time.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
|
||||
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
|
||||
OldNodeKey: tailcfg.NodeKeyFromNodePublic(oldNodeKey),
|
||||
NodeKey: tailcfg.NodeKeyFromNodePublic(tryingNewKey.Public()),
|
||||
Hostinfo: hostinfo,
|
||||
Followup: opt.URL,
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
@@ -440,13 +393,13 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterRequest: %s", j)
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &machinePrivKey)
|
||||
bodyData, err := encode(request, serverKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
req, err := http.NewRequest("POST", u, body)
|
||||
if err != nil {
|
||||
return regen, opt.URL, err
|
||||
@@ -464,7 +417,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, &serverKey, &machinePrivKey); err != nil {
|
||||
if err := decode(res, &resp, serverKey, machinePrivKey); err != nil {
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
@@ -477,6 +430,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
|
||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||
|
||||
if resp.Error != "" {
|
||||
return false, "", errors.New(resp.Error)
|
||||
}
|
||||
if resp.NodeKeyExpired {
|
||||
if regen {
|
||||
return true, "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
|
||||
@@ -639,7 +595,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
request := &tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentMapRequestVersion,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
NodeKey: tailcfg.NodeKeyFromNodePublic(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: epStrs,
|
||||
EndpointTypes: epTypes,
|
||||
@@ -678,7 +634,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
request.ReadOnly = true
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &machinePrivKey)
|
||||
bodyData, err := encode(request, serverKey, machinePrivKey)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
@@ -687,9 +643,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
machinePubKey := tailcfg.MachineKey(machinePrivKey.Public())
|
||||
machinePubKey := machinePrivKey.Public()
|
||||
t0 := time.Now()
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.UntypedHexString())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
@@ -776,7 +732,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
if err := c.decodeMsg(msg, &resp, &machinePrivKey); err != nil {
|
||||
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
|
||||
vlogf("netmap: decode error: %v")
|
||||
return err
|
||||
}
|
||||
@@ -830,28 +786,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return errors.New("MapResponse lacked node")
|
||||
}
|
||||
|
||||
// Temporarily (2020-06-29) support removing all but
|
||||
// discovery-supporting nodes during development, for
|
||||
// less noise.
|
||||
if Debug.OnlyDisco {
|
||||
anyOld, numDisco := false, 0
|
||||
for _, p := range nm.Peers {
|
||||
if p.DiscoKey.IsZero() {
|
||||
anyOld = true
|
||||
} else {
|
||||
numDisco++
|
||||
}
|
||||
}
|
||||
if anyOld {
|
||||
filtered := make([]*tailcfg.Node, 0, numDisco)
|
||||
for _, p := range nm.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
nm.Peers = filtered
|
||||
}
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
@@ -865,22 +799,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occured meanwhile. This only affects
|
||||
// a lite map update occurred meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
nm.LocalPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
// Code elsewhere prints netmap diffs every time, so this
|
||||
// occasional full dump, plus incremental diffs, should do
|
||||
// the job.
|
||||
// Occasionally print the netmap header.
|
||||
// This is handy for debugging, and our logs processing
|
||||
// pipeline depends on it. (TODO: Remove this dependency.)
|
||||
// Code elsewhere prints netmap diffs every time they are received.
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -895,7 +828,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
func decode(res *http.Response, v interface{}, serverKey key.MachinePublic, mkey key.MachinePrivate) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -914,14 +847,14 @@ var (
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey *wgkey.Private) error {
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey key.MachinePrivate) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
decrypted, err := decryptMsg(msg, &serverKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
var b []byte
|
||||
if c.newDecompressor == nil {
|
||||
@@ -953,10 +886,10 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}, machinePrivKey *wgkey.Priv
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, machinePrivKey *wgkey.Private) error {
|
||||
decrypted, err := decryptMsg(msg, serverKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey key.MachinePublic, machinePrivKey key.MachinePrivate) error {
|
||||
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
@@ -967,23 +900,7 @@ func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, machinePrivKey *
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if len(msg) < len(nonce)+1 {
|
||||
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
|
||||
}
|
||||
copy(nonce[:], msg)
|
||||
msg = msg[len(nonce):]
|
||||
|
||||
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
|
||||
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot decrypt response (len %d + nonce %d = %d)", len(msg), len(nonce), len(msg)+len(nonce))
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
func encode(v interface{}, serverKey key.MachinePublic, mkey key.MachinePrivate) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -993,38 +910,32 @@ func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, e
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
|
||||
msg := box.Seal(nonce[:], b, &nonce, pub, pri)
|
||||
return msg, nil
|
||||
return mkey.SealTo(serverKey, b), nil
|
||||
}
|
||||
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (key.MachinePublic, error) {
|
||||
req, err := http.NewRequest("GET", serverURL+"/key", nil)
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
return key.MachinePublic{}, fmt.Errorf("create control key request: %v", err)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return key.MachinePublic{}, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<16))
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
return key.MachinePublic{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
return key.MachinePublic{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
}
|
||||
key, err := wgkey.ParseHex(string(b))
|
||||
k, err := key.ParseMachinePublicUntyped(mem.B(b))
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return key.MachinePublic{}, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
return key, nil
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
@@ -1034,21 +945,18 @@ var Debug = initDebug()
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps bool // strip all local node's control-provided capabilities
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
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"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
Disco: os.Getenv("TS_DEBUG_USE_DISCO") == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1275,13 +1183,13 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
return errors.New("getMachinePrivKey returned zero key")
|
||||
}
|
||||
|
||||
bodyData, err := encode(req, &serverKey, &machinePrivKey)
|
||||
bodyData, err := encode(req, serverKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1296,10 +1204,66 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes struct{} // no fields yet
|
||||
if err := decode(res, &setDNSRes, &serverKey, &machinePrivKey); err != nil {
|
||||
if err := decode(res, &setDNSRes, serverKey, machinePrivKey); err != nil {
|
||||
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return fmt.Errorf("set-dns-response: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
||||
// with ping response data.
|
||||
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
||||
var err error
|
||||
if pr.URL == "" {
|
||||
return errors.New("invalid PingRequest with no URL")
|
||||
}
|
||||
if pr.IP.IsZero() {
|
||||
return errors.New("PingRequest without IP")
|
||||
}
|
||||
if !strings.Contains(pr.Types, "TSMP") {
|
||||
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
|
||||
// Currently does not check for error since we just return if it fails.
|
||||
err = postPingResult(now, logf, c, pr, res)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
|
||||
if res.Err != "" {
|
||||
return errors.New(res.Err)
|
||||
}
|
||||
duration := time.Since(now)
|
||||
if pr.Log {
|
||||
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
jsonPingRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Send the results of the Ping, back to control URL.
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
|
||||
}
|
||||
if pr.Log {
|
||||
logf("tsmpPing: sending ping results to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,27 +6,29 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
k := key.NewMachine()
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
@@ -56,7 +58,7 @@ func TestNewDirect(t *testing.T) {
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi = hostinfo.New()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
@@ -92,14 +94,52 @@ func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
func TestTsmpPing(t *testing.T) {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
k := key.NewMachine()
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pingRes := &ipnstate.PingResult{
|
||||
IP: "123.456.7890",
|
||||
Err: "",
|
||||
NodeName: "testnode",
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body := new(ipnstate.PingResult)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pingRes.IP != body.IP {
|
||||
t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
pr := &tailcfg.PingRequest{
|
||||
URL: ts.URL,
|
||||
}
|
||||
|
||||
err = postPingResult(now, t.Logf, c.httpc, pr, pingRes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -28,10 +28,10 @@ import (
|
||||
// one MapRequest).
|
||||
type mapSession struct {
|
||||
// Immutable fields.
|
||||
privateNodeKey wgkey.Private
|
||||
privateNodeKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey tailcfg.MachineKey
|
||||
machinePubKey key.MachinePublic
|
||||
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
|
||||
|
||||
// Fields storing state over the the coards of multiple MapResponses.
|
||||
@@ -43,13 +43,14 @@ type mapSession struct {
|
||||
collectServices bool
|
||||
previousPeers []*tailcfg.Node // for delta-purposes
|
||||
lastDomain string
|
||||
lastHealth []string
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
netMapBuilding *netmap.NetworkMap
|
||||
}
|
||||
|
||||
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
|
||||
func newMapSession(privateNodeKey key.NodePrivate) *mapSession {
|
||||
ms := &mapSession{
|
||||
privateNodeKey: privateNodeKey,
|
||||
logf: logger.Discard,
|
||||
@@ -104,9 +105,12 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
if resp.Domain != "" {
|
||||
ms.lastDomain = resp.Domain
|
||||
}
|
||||
if resp.Health != nil {
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
|
||||
NodeKey: tailcfg.NodeKeyFromNodePublic(ms.privateNodeKey.Public()),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
@@ -117,6 +121,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
@@ -170,11 +170,7 @@ func formatNodes(nodes []*tailcfg.Node) string {
|
||||
}
|
||||
|
||||
func newTestMapSession(t *testing.T) *mapSession {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newMapSession(k)
|
||||
return newMapSession(key.NewNode())
|
||||
}
|
||||
|
||||
func TestNetmapForResponse(t *testing.T) {
|
||||
@@ -218,7 +214,7 @@ func TestNetmapForResponse(t *testing.T) {
|
||||
}
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
DNSConfig: nil, // implict
|
||||
DNSConfig: nil, // implicit
|
||||
})
|
||||
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
|
||||
t.Fatalf("2nd DNS wrong")
|
||||
|
||||
@@ -10,22 +10,34 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoCertStore = errors.New("no certificate store")
|
||||
errCertificateNotConfigured = errors.New("no certificate subject configured")
|
||||
errNoCertStore = errors.New("no certificate store")
|
||||
errCertificateNotConfigured = errors.New("no certificate subject configured")
|
||||
errUnsupportedSignatureVersion = errors.New("unsupported signature version")
|
||||
)
|
||||
|
||||
// HashRegisterRequest generates the hash required sign or verify a
|
||||
// tailcfg.RegisterRequest with tailcfg.SignatureV1.
|
||||
func HashRegisterRequest(ts time.Time, serverURL string, deviceCert []byte, serverPubKey, machinePubKey wgkey.Key) []byte {
|
||||
// tailcfg.RegisterRequest.
|
||||
func HashRegisterRequest(
|
||||
version tailcfg.SignatureType, ts time.Time, serverURL string, deviceCert []byte,
|
||||
serverPubKey, machinePubKey key.MachinePublic) ([]byte, error) {
|
||||
h := crypto.SHA256.New()
|
||||
|
||||
// hash.Hash.Write never returns an error, so we don't check for one here.
|
||||
fmt.Fprintf(h, "%s%s%s%s%s",
|
||||
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
|
||||
switch version {
|
||||
case tailcfg.SignatureV1:
|
||||
fmt.Fprintf(h, "%s%s%s%s%s",
|
||||
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey.ShortString(), machinePubKey.ShortString())
|
||||
case tailcfg.SignatureV2:
|
||||
fmt.Fprintf(h, "%s%s%s%s%s",
|
||||
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
|
||||
default:
|
||||
return nil, errUnsupportedSignatureVersion
|
||||
}
|
||||
|
||||
return h.Sum(nil)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && cgo
|
||||
// +build windows,cgo
|
||||
|
||||
// darwin,cgo is also supported by certstore but machineCertificateSubject will
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
@@ -124,7 +125,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
|
||||
// using that identity's public key. In addition to the signature, the full
|
||||
// certificate chain is included so that the control server can validate the
|
||||
// certificate from a copy of the root CA's certificate.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) (err error) {
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signRegisterRequest: %w", err)
|
||||
@@ -166,7 +167,11 @@ func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverP
|
||||
req.DeviceCert = append(req.DeviceCert, c.Raw...)
|
||||
}
|
||||
|
||||
h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
|
||||
req.SignatureType = tailcfg.SignatureV2
|
||||
h, err := HashRegisterRequest(req.SignatureType, req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash: %w", err)
|
||||
}
|
||||
|
||||
req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
@@ -175,7 +180,6 @@ func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverP
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: %w", err)
|
||||
}
|
||||
req.SignatureType = tailcfg.SignatureV1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows || !cgo
|
||||
// +build !windows !cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// signRegisterRequest on non-supported platforms always returns errNoCertStore.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error {
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
|
||||
return errNoCertStore
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
LogoutFinished *empty.Message // nonempty when logout finishes
|
||||
Err string
|
||||
Err error
|
||||
URL string // interactive URL to visit to finish logging in
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
|
||||
|
||||
14
derp/derp.go
14
derp/derp.go
@@ -101,6 +101,20 @@ const (
|
||||
|
||||
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
|
||||
// frameHealth is sent from server to client to tell the client
|
||||
// if their connection is unhealthy somehow. Currently the only unhealthy state
|
||||
// is whether the connection is detected as a duplicate.
|
||||
// The entire frame body is the text of the error message. An empty message
|
||||
// clears the error state.
|
||||
frameHealth = frameType(0x14)
|
||||
|
||||
// frameRestarting is sent from server to client for the
|
||||
// server to declare that it's restarting. Payload is two big
|
||||
// endian uint32 durations in milliseconds: when to reconnect,
|
||||
// and how long to try total. See ServerRestartingMessage docs for
|
||||
// more details on how the client should interpret them.
|
||||
frameRestarting = frameType(0x15)
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -6,7 +6,7 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,16 +14,17 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
serverKey key.NodePublic // of the DERP server; not a machine or node key
|
||||
privateKey key.NodePrivate
|
||||
publicKey key.NodePublic // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
@@ -31,8 +32,9 @@ type Client struct {
|
||||
canAckPings bool
|
||||
isProber bool
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
rate *rate.Limiter // if non-nil, rate limiter to use
|
||||
|
||||
// Owned by Recv:
|
||||
peeked int // bytes to discard on next Recv
|
||||
@@ -51,7 +53,7 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
ServerPub key.NodePublic
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.Is
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
func ServerPublicKey(key key.NodePublic) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ func CanAckPings(v bool) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
func NewClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
if o == nil {
|
||||
@@ -89,7 +91,7 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
return newClient(privateKey, nc, brw, logf, opt)
|
||||
}
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
@@ -127,7 +129,7 @@ func (c *Client) recvServerKey() error {
|
||||
if flen < uint32(len(buf)) || t != frameServerKey || string(buf[:len(magic)]) != magic {
|
||||
return errors.New("invalid server greeting")
|
||||
}
|
||||
copy(c.serverKey[:], buf[len(magic):])
|
||||
c.serverKey = key.NodePublicFromRaw32(mem.B(buf[len(magic):]))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,13 +142,9 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
if fl > maxLength {
|
||||
return nil, fmt.Errorf("long serverInfo frame")
|
||||
}
|
||||
// TODO: add a read-nonce-and-box helper
|
||||
var nonce [nonceLen]byte
|
||||
copy(nonce[:], b)
|
||||
msgbox := b[nonceLen:]
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
msg, ok := c.privateKey.OpenFrom(c.serverKey, b)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %x", c.serverKey[:])
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %s", c.serverKey)
|
||||
}
|
||||
info := new(serverInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
@@ -173,10 +171,6 @@ type clientInfo struct {
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
var nonce [nonceLen]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
@@ -186,24 +180,23 @@ func (c *Client) sendClientKey() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgbox := box.Seal(nil, msg, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
msgbox := c.privateKey.SealTo(c.serverKey, msg)
|
||||
|
||||
buf := make([]byte, 0, nonceLen+keyLen+len(msgbox))
|
||||
buf = append(buf, c.publicKey[:]...)
|
||||
buf = append(buf, nonce[:]...)
|
||||
buf := make([]byte, 0, keyLen+len(msgbox))
|
||||
buf = c.publicKey.AppendTo(buf)
|
||||
buf = append(buf, msgbox...)
|
||||
return writeFrame(c.bw, frameClientInfo, buf)
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
func (c *Client) ServerPublicKey() key.Public { return c.serverKey }
|
||||
func (c *Client) ServerPublicKey() key.NodePublic { return c.serverKey }
|
||||
|
||||
// Send sends a packet to the Tailscale node identified by dstKey.
|
||||
//
|
||||
// It is an error if the packet is larger than 64KB.
|
||||
func (c *Client) Send(dstKey key.Public, pkt []byte) error { return c.send(dstKey, pkt) }
|
||||
func (c *Client) Send(dstKey key.NodePublic, pkt []byte) error { return c.send(dstKey, pkt) }
|
||||
|
||||
func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
ret = fmt.Errorf("derp.Send: %w", ret)
|
||||
@@ -216,11 +209,16 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(len(dstKey)+len(pkt))); err != nil {
|
||||
if c.rate != nil {
|
||||
pktLen := frameHeaderLen + dstKey.RawLen() + len(pkt)
|
||||
if !c.rate.AllowN(time.Now(), pktLen) {
|
||||
return nil // drop
|
||||
}
|
||||
}
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(dstKey.RawLen()+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(pkt); err != nil {
|
||||
@@ -229,7 +227,7 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error) {
|
||||
func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.ForwardPacket: %w", err)
|
||||
@@ -249,10 +247,10 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
|
||||
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(srcKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(srcKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey[:]); err != nil {
|
||||
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(pkt); err != nil {
|
||||
@@ -314,10 +312,10 @@ func (c *Client) WatchConnectionChanges() error {
|
||||
|
||||
// ClosePeer asks the server to close target's TCP connection.
|
||||
// It's a fatal error if the client wasn't created using MeshKey.
|
||||
func (c *Client) ClosePeer(target key.Public) error {
|
||||
func (c *Client) ClosePeer(target key.NodePublic) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
return writeFrame(c.bw, frameClosePeer, target[:])
|
||||
return writeFrame(c.bw, frameClosePeer, target.AppendTo(nil))
|
||||
}
|
||||
|
||||
// ReceivedMessage represents a type returned by Client.Recv. Unless
|
||||
@@ -330,7 +328,7 @@ type ReceivedMessage interface {
|
||||
|
||||
// ReceivedPacket is a ReceivedMessage representing an incoming packet.
|
||||
type ReceivedPacket struct {
|
||||
Source key.Public
|
||||
Source key.NodePublic
|
||||
// Data is the received packet bytes. It aliases the memory
|
||||
// passed to Client.Recv.
|
||||
Data []byte
|
||||
@@ -341,18 +339,33 @@ func (ReceivedPacket) msg() {}
|
||||
// PeerGoneMessage is a ReceivedMessage that indicates that the client
|
||||
// identified by the underlying public key had previously sent you a
|
||||
// packet but has now disconnected from the server.
|
||||
type PeerGoneMessage key.Public
|
||||
type PeerGoneMessage key.NodePublic
|
||||
|
||||
func (PeerGoneMessage) msg() {}
|
||||
|
||||
// PeerPresentMessage is a ReceivedMessage that indicates that the client
|
||||
// is connected to the server. (Only used by trusted mesh clients)
|
||||
type PeerPresentMessage key.Public
|
||||
type PeerPresentMessage key.NodePublic
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
|
||||
// ServerInfoMessage is sent by the server upon first connect.
|
||||
type ServerInfoMessage struct{}
|
||||
type ServerInfoMessage struct {
|
||||
// TokenBucketBytesPerSecond is how many bytes per second the
|
||||
// server says it will accept, including all framing bytes.
|
||||
//
|
||||
// Zero means unspecified. There might be a limit, but the
|
||||
// client need not try to respect it.
|
||||
TokenBucketBytesPerSecond int
|
||||
|
||||
// TokenBucketBytesBurst is how many bytes the server will
|
||||
// allow to burst, temporarily violating
|
||||
// TokenBucketBytesPerSecond.
|
||||
//
|
||||
// Zero means unspecified. There might be a limit, but the
|
||||
// client need not try to respect it.
|
||||
TokenBucketBytesBurst int
|
||||
}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
@@ -369,6 +382,40 @@ type KeepAliveMessage struct{}
|
||||
|
||||
func (KeepAliveMessage) msg() {}
|
||||
|
||||
// HealthMessage is a one-way message from server to client, declaring the
|
||||
// connection health state.
|
||||
type HealthMessage struct {
|
||||
// Problem, if non-empty, is a description of why the connection
|
||||
// is unhealthy.
|
||||
//
|
||||
// The empty string means the connection is healthy again.
|
||||
//
|
||||
// The default condition is healthy, so the server doesn't
|
||||
// broadcast a HealthMessage until a problem exists.
|
||||
Problem string
|
||||
}
|
||||
|
||||
func (HealthMessage) msg() {}
|
||||
|
||||
// ServerRestartingMessage is a one-way message from server to client,
|
||||
// advertising that the server is restarting.
|
||||
type ServerRestartingMessage struct {
|
||||
// ReconnectIn is an advisory duration that the client should wait
|
||||
// before attempting to reconnect. It might be zero.
|
||||
// It exists for the server to smear out the reconnects.
|
||||
ReconnectIn time.Duration
|
||||
|
||||
// TryFor is an advisory duration for how long the client
|
||||
// should attempt to reconnect before giving up and proceeding
|
||||
// with its normal connection failure logic. The interval
|
||||
// between retries is undefined for now.
|
||||
// A server should not send a TryFor duration more than a few
|
||||
// seconds.
|
||||
TryFor time.Duration
|
||||
}
|
||||
|
||||
func (ServerRestartingMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -440,12 +487,16 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// needing to wait an RTT to discover the version at startup.
|
||||
// We'd prefer to give the connection to the client (magicsock)
|
||||
// to start writing as soon as possible.
|
||||
_, err := c.parseServerInfo(b)
|
||||
si, err := c.parseServerInfo(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid server info frame: %v", err)
|
||||
}
|
||||
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
|
||||
return ServerInfoMessage{}, nil
|
||||
sm := ServerInfoMessage{
|
||||
TokenBucketBytesPerSecond: si.TokenBucketBytesPerSecond,
|
||||
TokenBucketBytesBurst: si.TokenBucketBytesBurst,
|
||||
}
|
||||
c.setSendRateLimiter(sm)
|
||||
return sm, nil
|
||||
case frameKeepAlive:
|
||||
// A one-way keep-alive message that doesn't require an acknowledgement.
|
||||
// This predated framePing/framePong.
|
||||
@@ -455,8 +506,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerGoneMessage
|
||||
copy(pg[:], b[:keyLen])
|
||||
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
@@ -464,8 +514,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerPresentMessage
|
||||
copy(pg[:], b[:keyLen])
|
||||
pg := PeerPresentMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
|
||||
return pg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
@@ -474,7 +523,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Source = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
|
||||
@@ -486,6 +535,32 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case frameHealth:
|
||||
return HealthMessage{Problem: string(b[:])}, nil
|
||||
|
||||
case frameRestarting:
|
||||
var m ServerRestartingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short server restarting frame")
|
||||
continue
|
||||
}
|
||||
m.ReconnectIn = time.Duration(binary.BigEndian.Uint32(b[0:4])) * time.Millisecond
|
||||
m.TryFor = time.Duration(binary.BigEndian.Uint32(b[4:8])) * time.Millisecond
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
if sm.TokenBucketBytesPerSecond == 0 {
|
||||
c.rate = nil
|
||||
} else {
|
||||
c.rate = rate.NewLimiter(
|
||||
rate.Limit(sm.TokenBucketBytesPerSecond),
|
||||
sm.TokenBucketBytesBurst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,16 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/pad32"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.Public]bool{}
|
||||
var verboseDropKeys = map[key.NodePublic]bool{}
|
||||
|
||||
func init() {
|
||||
keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
|
||||
@@ -58,7 +59,7 @@ func init() {
|
||||
return
|
||||
}
|
||||
for _, keyStr := range strings.Split(keys, ",") {
|
||||
k, err := key.NewPublicFromHexMem(mem.S(keyStr))
|
||||
k, err := key.ParseNodePublicUntyped(mem.S(keyStr))
|
||||
if err != nil {
|
||||
log.Printf("ignoring invalid debug key %q: %v", keyStr, err)
|
||||
} else {
|
||||
@@ -76,12 +77,20 @@ const (
|
||||
writeTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
const host64bit = (^uint(0) >> 32) & 1 // 1 on 64-bit, 0 on 32-bit
|
||||
// dupPolicy is a temporary (2021-08-30) mechanism to change the policy
|
||||
// of how duplicate connection for the same key are handled.
|
||||
type dupPolicy int8
|
||||
|
||||
// pad32bit is 4 on 32-bit machines and 0 on 64-bit.
|
||||
// It exists so the Server struct's atomic fields can be aligned to 8
|
||||
// byte boundaries. (As tested by GOARCH=386 go test, etc)
|
||||
const pad32bit = 4 - host64bit*4 // 0 on 64-bit, 4 on 32-bit
|
||||
const (
|
||||
// lastWriterIsActive is a dupPolicy where the connection
|
||||
// to send traffic for a peer is the active one.
|
||||
lastWriterIsActive dupPolicy = iota
|
||||
|
||||
// disableFighters is a dupPolicy that detects if peers
|
||||
// are trying to send interleaved with each other and
|
||||
// then disables all of them.
|
||||
disableFighters
|
||||
)
|
||||
|
||||
// Server is a DERP server.
|
||||
type Server struct {
|
||||
@@ -89,38 +98,38 @@ type Server struct {
|
||||
// before failing when writing to a client.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
privateKey key.NodePrivate
|
||||
publicKey key.NodePublic
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
limitedLogf logger.Logf
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
dupPolicy dupPolicy
|
||||
|
||||
// Counters:
|
||||
_ [pad32bit]byte
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
_ pad32.Four
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
|
||||
packetsDroppedType metrics.LabelMap
|
||||
packetsDroppedTypeDisco *expvar.Int
|
||||
packetsDroppedTypeOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
_ pad32.Four
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
clientsReplaced expvar.Int
|
||||
clientsReplaceLimited expvar.Int
|
||||
clientsReplaceSleeping expvar.Int
|
||||
dupClientKeys expvar.Int // current number of public keys we have 2+ connections for
|
||||
dupClientConns expvar.Int // current number of connections sharing a public key
|
||||
dupClientConnTotal expvar.Int // total number of accepted connections when a dup key existed
|
||||
unknownFrames expvar.Int
|
||||
homeMovesIn expvar.Int // established clients announce home server moves in
|
||||
homeMovesOut expvar.Int // established clients announce home server moves out
|
||||
@@ -136,32 +145,138 @@ type Server struct {
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]*sclient
|
||||
clients map[key.NodePublic]clientSet
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
// clientsMesh tracks all clients in the cluster, both locally
|
||||
// and to mesh peers. If the value is nil, that means the
|
||||
// peer is only local (and thus in the clients Map, but not
|
||||
// remote). If the value is non-nil, it's remote (+ maybe also
|
||||
// local).
|
||||
clientsMesh map[key.Public]PacketForwarder
|
||||
clientsMesh map[key.NodePublic]PacketForwarder
|
||||
// sentTo tracks which peers have sent to which other peers,
|
||||
// and at which connection number. This isn't on sclient
|
||||
// because it includes intra-region forwarded packets as the
|
||||
// src.
|
||||
sentTo map[key.Public]map[key.Public]int64 // src => dst => dst's latest sclient.connNum
|
||||
sentTo map[key.NodePublic]map[key.NodePublic]int64 // src => dst => dst's latest sclient.connNum
|
||||
|
||||
// maps from netaddr.IPPort to a client's public key
|
||||
keyOfAddr map[netaddr.IPPort]key.Public
|
||||
keyOfAddr map[netaddr.IPPort]key.NodePublic
|
||||
}
|
||||
|
||||
// clientSet represents 1 or more *sclients.
|
||||
//
|
||||
// The two implementations are singleClient and *dupClientSet.
|
||||
//
|
||||
// In the common case, client should only have one connection to the
|
||||
// DERP server for a given key. When they're connected multiple times,
|
||||
// we record their set of connections in dupClientSet and keep their
|
||||
// connections open to make them happy (to keep them from spinning,
|
||||
// etc) and keep track of which is the latest connection. If only the last
|
||||
// is sending traffic, that last one is the active connection and it
|
||||
// gets traffic. Otherwise, in the case of a cloned node key, the
|
||||
// whole set of dups doesn't receive data frames.
|
||||
//
|
||||
// All methods should only be called while holding Server.mu.
|
||||
//
|
||||
// TODO(bradfitz): Issue 2746: in the future we'll send some sort of
|
||||
// "health_error" frame to them that'll communicate to the end users
|
||||
// that they cloned a device key, and we'll also surface it in the
|
||||
// admin panel, etc.
|
||||
type clientSet interface {
|
||||
// ActiveClient returns the most recently added client to
|
||||
// the set, as long as it hasn't been disabled, in which
|
||||
// case it returns nil.
|
||||
ActiveClient() *sclient
|
||||
|
||||
// Len returns the number of clients in the set.
|
||||
Len() int
|
||||
|
||||
// ForeachClient calls f for each client in the set.
|
||||
ForeachClient(f func(*sclient))
|
||||
}
|
||||
|
||||
// singleClient is a clientSet of a single connection.
|
||||
// This is the common case.
|
||||
type singleClient struct{ c *sclient }
|
||||
|
||||
func (s singleClient) ActiveClient() *sclient { return s.c }
|
||||
func (s singleClient) Len() int { return 1 }
|
||||
func (s singleClient) ForeachClient(f func(*sclient)) { f(s.c) }
|
||||
|
||||
// A dupClientSet is a clientSet of more than 1 connection.
|
||||
//
|
||||
// This can occur in some reasonable cases (temporarily while users
|
||||
// are changing networks) or in the case of a cloned key. In the
|
||||
// cloned key case, both peers are speaking and the clients get
|
||||
// disabled.
|
||||
//
|
||||
// All fields are guarded by Server.mu.
|
||||
type dupClientSet struct {
|
||||
// set is the set of connected clients for sclient.key.
|
||||
// The values are all true.
|
||||
set map[*sclient]bool
|
||||
|
||||
// last is the most recent addition to set, or nil if the most
|
||||
// recent one has since disconnected and nobody else has send
|
||||
// data since.
|
||||
last *sclient
|
||||
|
||||
// sendHistory is a log of which members of set have sent
|
||||
// frames to the derp server, with adjacent duplicates
|
||||
// removed. When a member of set is removed, the same
|
||||
// element(s) are removed from sendHistory.
|
||||
sendHistory []*sclient
|
||||
}
|
||||
|
||||
func (s *dupClientSet) ActiveClient() *sclient {
|
||||
if s.last != nil && !s.last.isDisabled.Get() {
|
||||
return s.last
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *dupClientSet) Len() int { return len(s.set) }
|
||||
func (s *dupClientSet) ForeachClient(f func(*sclient)) {
|
||||
for c := range s.set {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
|
||||
// removeClient removes c from s and reports whether it was in s
|
||||
// to begin with.
|
||||
func (s *dupClientSet) removeClient(c *sclient) bool {
|
||||
n := len(s.set)
|
||||
delete(s.set, c)
|
||||
if s.last == c {
|
||||
s.last = nil
|
||||
}
|
||||
if len(s.set) == n {
|
||||
return false
|
||||
}
|
||||
|
||||
trim := s.sendHistory[:0]
|
||||
for _, v := range s.sendHistory {
|
||||
if s.set[v] && (len(trim) == 0 || trim[len(trim)-1] != v) {
|
||||
trim = append(trim, v)
|
||||
}
|
||||
}
|
||||
for i := len(trim); i < len(s.sendHistory); i++ {
|
||||
s.sendHistory[i] = nil
|
||||
}
|
||||
s.sendHistory = trim
|
||||
if s.last == nil && len(s.sendHistory) > 0 {
|
||||
s.last = s.sendHistory[len(s.sendHistory)-1]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// PacketForwarder is something that can forward packets.
|
||||
//
|
||||
// It's mostly an inteface for circular dependency reasons; the
|
||||
// It's mostly an interface for circular dependency reasons; the
|
||||
// typical implementation is derphttp.Client. The other implementation
|
||||
// is a multiForwarder, which this package creates as needed if a
|
||||
// public key gets more than one PacketForwarder registered for it.
|
||||
type PacketForwarder interface {
|
||||
ForwardPacket(src, dst key.Public, payload []byte) error
|
||||
ForwardPacket(src, dst key.NodePublic, payload []byte) error
|
||||
}
|
||||
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
@@ -178,7 +293,7 @@ type Conn interface {
|
||||
|
||||
// NewServer returns a new DERP server. It doesn't listen on its own.
|
||||
// Connections are given to it via Server.Accept.
|
||||
func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
|
||||
@@ -190,14 +305,14 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
packetsDroppedType: metrics.LabelMap{Label: "type"},
|
||||
clients: map[key.Public]*sclient{},
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
clients: map[key.NodePublic]clientSet{},
|
||||
clientsMesh: map[key.NodePublic]PacketForwarder{},
|
||||
netConns: map[Conn]chan struct{}{},
|
||||
memSys0: ms.Sys,
|
||||
watchers: map[*sclient]bool{},
|
||||
sentTo: map[key.Public]map[key.Public]int64{},
|
||||
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
|
||||
avgQueueDuration: new(uint64),
|
||||
keyOfAddr: map[netaddr.IPPort]key.Public{},
|
||||
keyOfAddr: map[netaddr.IPPort]key.NodePublic{},
|
||||
}
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
@@ -237,10 +352,10 @@ func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
|
||||
func (s *Server) MeshKey() string { return s.meshKey }
|
||||
|
||||
// PrivateKey returns the server's private key.
|
||||
func (s *Server) PrivateKey() key.Private { return s.privateKey }
|
||||
func (s *Server) PrivateKey() key.NodePrivate { return s.privateKey }
|
||||
|
||||
// PublicKey returns the server's public key.
|
||||
func (s *Server) PublicKey() key.Public { return s.publicKey }
|
||||
func (s *Server) PublicKey() key.NodePublic { return s.publicKey }
|
||||
|
||||
// Close closes the server and waits for the connections to disconnect.
|
||||
func (s *Server) Close() error {
|
||||
@@ -331,7 +446,7 @@ func (s *Server) initMetacert() {
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(ProtocolVersion),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("derpkey%x", s.publicKey[:]),
|
||||
CommonName: fmt.Sprintf("derpkey%s", s.publicKey.UntypedHexString()),
|
||||
},
|
||||
// Windows requires NotAfter and NotBefore set:
|
||||
NotAfter: time.Now().Add(30 * 24 * time.Hour),
|
||||
@@ -350,39 +465,48 @@ func (s *Server) MetaCert() []byte { return s.metaCert }
|
||||
|
||||
// registerClient notes that client c is now authenticated and ready for packets.
|
||||
//
|
||||
// If c's public key was already connected with a different
|
||||
// connection, the prior one is closed, unless it's fighting rapidly
|
||||
// with another client with the same key, in which case the returned
|
||||
// ok is false, and the caller should wait the provided duration
|
||||
// before trying again.
|
||||
func (s *Server) registerClient(c *sclient) (ok bool, d time.Duration) {
|
||||
// If c.key is connected more than once, the earlier connection(s) are
|
||||
// placed in a non-active state where we read from them (primarily to
|
||||
// observe EOFs/timeouts) but won't send them frames on the assumption
|
||||
// that they're dead.
|
||||
func (s *Server) registerClient(c *sclient) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.clients[c.key]
|
||||
if old == nil {
|
||||
c.logf("adding connection")
|
||||
} else {
|
||||
// Take over the old rate limiter, discarding the one
|
||||
// our caller just made.
|
||||
c.replaceLimiter = old.replaceLimiter
|
||||
if rr := c.replaceLimiter.ReserveN(timeNow(), 1); rr.OK() {
|
||||
if d := rr.DelayFrom(timeNow()); d > 0 {
|
||||
s.clientsReplaceLimited.Add(1)
|
||||
return false, d
|
||||
}
|
||||
|
||||
set := s.clients[c.key]
|
||||
switch set := set.(type) {
|
||||
case nil:
|
||||
s.clients[c.key] = singleClient{c}
|
||||
case singleClient:
|
||||
s.dupClientKeys.Add(1)
|
||||
s.dupClientConns.Add(2) // both old and new count
|
||||
s.dupClientConnTotal.Add(1)
|
||||
old := set.ActiveClient()
|
||||
old.isDup.Set(true)
|
||||
c.isDup.Set(true)
|
||||
s.clients[c.key] = &dupClientSet{
|
||||
last: c,
|
||||
set: map[*sclient]bool{
|
||||
old: true,
|
||||
c: true,
|
||||
},
|
||||
sendHistory: []*sclient{old},
|
||||
}
|
||||
s.clientsReplaced.Add(1)
|
||||
c.logf("adding connection, replacing %s", old.remoteAddr)
|
||||
go old.nc.Close()
|
||||
case *dupClientSet:
|
||||
s.dupClientConns.Add(1) // the gauge
|
||||
s.dupClientConnTotal.Add(1) // the counter
|
||||
c.isDup.Set(true)
|
||||
set.set[c] = true
|
||||
set.last = c
|
||||
set.sendHistory = append(set.sendHistory, c)
|
||||
}
|
||||
s.clients[c.key] = c
|
||||
|
||||
if _, ok := s.clientsMesh[c.key]; !ok {
|
||||
s.clientsMesh[c.key] = nil // just for varz of total users in cluster
|
||||
}
|
||||
s.keyOfAddr[c.remoteIPPort] = c.key
|
||||
s.curClients.Add(1)
|
||||
s.broadcastPeerStateChangeLocked(c.key, true)
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// broadcastPeerStateChangeLocked enqueues a message to all watchers
|
||||
@@ -390,7 +514,7 @@ func (s *Server) registerClient(c *sclient) (ok bool, d time.Duration) {
|
||||
// presence changed.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.Public, present bool) {
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, present bool) {
|
||||
for w := range s.watchers {
|
||||
w.peerStateChange = append(w.peerStateChange, peerConnState{peer: peer, present: present})
|
||||
go w.requestMeshUpdate()
|
||||
@@ -401,8 +525,12 @@ func (s *Server) broadcastPeerStateChangeLocked(peer key.Public, present bool) {
|
||||
func (s *Server) unregisterClient(c *sclient) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
cur := s.clients[c.key]
|
||||
if cur == c {
|
||||
|
||||
set := s.clients[c.key]
|
||||
switch set := set.(type) {
|
||||
case nil:
|
||||
c.logf("[unexpected]; clients map is empty")
|
||||
case singleClient:
|
||||
c.logf("removing connection")
|
||||
delete(s.clients, c.key)
|
||||
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
|
||||
@@ -410,7 +538,28 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
s.notePeerGoneFromRegionLocked(c.key)
|
||||
}
|
||||
s.broadcastPeerStateChangeLocked(c.key, false)
|
||||
case *dupClientSet:
|
||||
if set.removeClient(c) {
|
||||
s.dupClientConns.Add(-1)
|
||||
} else {
|
||||
c.logf("[unexpected]; dup client set didn't shrink")
|
||||
}
|
||||
if set.Len() == 1 {
|
||||
s.dupClientConns.Add(-1) // again; for the original one's
|
||||
s.dupClientKeys.Add(-1)
|
||||
var remain *sclient
|
||||
for remain = range set.set {
|
||||
break
|
||||
}
|
||||
if remain == nil {
|
||||
panic("unexpected nil remain from single element dup set")
|
||||
}
|
||||
remain.isDisabled.Set(false)
|
||||
remain.isDup.Set(false)
|
||||
s.clients[c.key] = singleClient{remain}
|
||||
}
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
delete(s.watchers, c)
|
||||
}
|
||||
@@ -427,7 +576,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
// key has sent to previously (whether those sends were from a local
|
||||
// client or forwarded). It must only be called after the key has
|
||||
// been removed from clientsMesh.
|
||||
func (s *Server) notePeerGoneFromRegionLocked(key key.Public) {
|
||||
func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
|
||||
if _, ok := s.clientsMesh[key]; ok {
|
||||
panic("usage")
|
||||
}
|
||||
@@ -437,9 +586,15 @@ func (s *Server) notePeerGoneFromRegionLocked(key key.Public) {
|
||||
// or move them over to the active client (in case a replaced client
|
||||
// connection is being unregistered).
|
||||
for pubKey, connNum := range s.sentTo[key] {
|
||||
if peer, ok := s.clients[pubKey]; ok && peer.connNum == connNum {
|
||||
go peer.requestPeerGoneWrite(key)
|
||||
set, ok := s.clients[pubKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
set.ForeachClient(func(peer *sclient) {
|
||||
if peer.connNum == connNum {
|
||||
go peer.requestPeerGoneWrite(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
delete(s.sentTo, key)
|
||||
}
|
||||
@@ -507,14 +662,8 @@ 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),
|
||||
peerGone: make(chan key.Public),
|
||||
peerGone: make(chan key.NodePublic),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
|
||||
// Allow kicking out previous connections once a
|
||||
// minute, with a very high burst of 100. Once a
|
||||
// minute is less than the client's 2 minute
|
||||
// inactivity timeout.
|
||||
replaceLimiter: rate.NewLimiter(rate.Every(time.Minute), 100),
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
@@ -524,15 +673,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
c.info = *clientInfo
|
||||
}
|
||||
|
||||
for {
|
||||
ok, d := s.registerClient(c)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
s.clientsReplaceSleeping.Add(1)
|
||||
timeSleep(d)
|
||||
s.clientsReplaceSleeping.Add(-1)
|
||||
}
|
||||
s.registerClient(c)
|
||||
defer s.unregisterClient(c)
|
||||
|
||||
err = s.sendServerInfo(c.bw, clientKey)
|
||||
@@ -576,6 +717,7 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
}
|
||||
return fmt.Errorf("client %x: readFrameHeader: %w", c.key, err)
|
||||
}
|
||||
c.s.noteClientActivity(c)
|
||||
switch ft {
|
||||
case frameNotePreferred:
|
||||
err = c.handleFrameNotePreferred(ft, fl)
|
||||
@@ -631,8 +773,8 @@ func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
|
||||
if !c.canMesh {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
var targetKey key.Public
|
||||
if _, err := io.ReadFull(c.br, targetKey[:]); err != nil {
|
||||
var targetKey key.NodePublic
|
||||
if err := targetKey.ReadRawWithoutAllocating(c.br); err != nil {
|
||||
return err
|
||||
}
|
||||
s := c.s
|
||||
@@ -640,9 +782,15 @@ func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if target, ok := s.clients[targetKey]; ok {
|
||||
c.logf("frameClosePeer closing peer %x", targetKey)
|
||||
go target.nc.Close()
|
||||
if set, ok := s.clients[targetKey]; ok {
|
||||
if set.Len() == 1 {
|
||||
c.logf("frameClosePeer closing peer %x", targetKey)
|
||||
} else {
|
||||
c.logf("frameClosePeer closing peer %x (%d connections)", targetKey, set.Len())
|
||||
}
|
||||
set.ForeachClient(func(target *sclient) {
|
||||
go target.nc.Close()
|
||||
})
|
||||
} else {
|
||||
c.logf("frameClosePeer failed to find peer %x", targetKey)
|
||||
}
|
||||
@@ -664,15 +812,25 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
s.packetsForwardedIn.Add(1)
|
||||
|
||||
var dstLen int
|
||||
var dst *sclient
|
||||
|
||||
s.mu.Lock()
|
||||
dst := s.clients[dstKey]
|
||||
if set, ok := s.clients[dstKey]; ok {
|
||||
dstLen = set.Len()
|
||||
dst = set.ActiveClient()
|
||||
}
|
||||
if dst != nil {
|
||||
s.notePeerSendLocked(srcKey, dst)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if dst == nil {
|
||||
s.recordDrop(contents, srcKey, dstKey, dropReasonUnknownDestOnFwd)
|
||||
reason := dropReasonUnknownDestOnFwd
|
||||
if dstLen > 1 {
|
||||
reason = dropReasonDupClient
|
||||
}
|
||||
s.recordDrop(contents, srcKey, dstKey, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -686,10 +844,10 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
// notePeerSendLocked records that src sent to dst. We keep track of
|
||||
// that so when src disconnects, we can tell dst (if it's still
|
||||
// around) that src is gone (a peerGone frame).
|
||||
func (s *Server) notePeerSendLocked(src key.Public, dst *sclient) {
|
||||
func (s *Server) notePeerSendLocked(src key.NodePublic, dst *sclient) {
|
||||
m, ok := s.sentTo[src]
|
||||
if !ok {
|
||||
m = map[key.Public]int64{}
|
||||
m = map[key.NodePublic]int64{}
|
||||
s.sentTo[src] = m
|
||||
}
|
||||
m[dst.key] = dst.connNum
|
||||
@@ -705,12 +863,18 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
|
||||
var fwd PacketForwarder
|
||||
var dstLen int
|
||||
var dst *sclient
|
||||
|
||||
s.mu.Lock()
|
||||
dst := s.clients[dstKey]
|
||||
if dst == nil {
|
||||
fwd = s.clientsMesh[dstKey]
|
||||
} else {
|
||||
if set, ok := s.clients[dstKey]; ok {
|
||||
dstLen = set.Len()
|
||||
dst = set.ActiveClient()
|
||||
}
|
||||
if dst != nil {
|
||||
s.notePeerSendLocked(c.key, dst)
|
||||
} else if dstLen < 1 {
|
||||
fwd = s.clientsMesh[dstKey]
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -723,7 +887,11 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
s.recordDrop(contents, c.key, dstKey, dropReasonUnknownDest)
|
||||
reason := dropReasonUnknownDest
|
||||
if dstLen > 1 {
|
||||
reason = dropReasonDupClient
|
||||
}
|
||||
s.recordDrop(contents, c.key, dstKey, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -747,9 +915,10 @@ const (
|
||||
dropReasonQueueHead // destination queue is full, dropped packet at queue head
|
||||
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
|
||||
dropReasonWriteError // OS write() failed
|
||||
dropReasonDupClient // the public key is connected 2+ times (active/active, fighting)
|
||||
)
|
||||
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.Public, reason dropReason) {
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedReasonCounters[reason].Add(1)
|
||||
if disco.LooksLikeDiscoWrapper(packetBytes) {
|
||||
@@ -812,7 +981,7 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// requestPeerGoneWrite sends a request to write a "peer gone" frame
|
||||
// that the provided peer has disconnected. It blocks until either the
|
||||
// write request is scheduled, or the client has closed.
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.Public) {
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
|
||||
select {
|
||||
case c.peerGone <- peer:
|
||||
case <-c.done:
|
||||
@@ -829,7 +998,7 @@ func (c *sclient) requestMeshUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
func (s *Server) verifyClient(clientKey key.NodePublic, info *clientInfo) error {
|
||||
if !s.verifyClients {
|
||||
return nil
|
||||
}
|
||||
@@ -848,33 +1017,80 @@ func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
}
|
||||
|
||||
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
|
||||
buf := make([]byte, 0, len(magic)+len(s.publicKey))
|
||||
buf := make([]byte, 0, len(magic)+s.publicKey.RawLen())
|
||||
buf = append(buf, magic...)
|
||||
buf = append(buf, s.publicKey[:]...)
|
||||
buf = s.publicKey.AppendTo(buf)
|
||||
err := writeFrame(lw.bw(), frameServerKey, buf)
|
||||
lw.Flush() // redundant (no-op) flush to release bufio.Writer
|
||||
return err
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
func (s *Server) noteClientActivity(c *sclient) {
|
||||
if !c.isDup.Get() {
|
||||
// Fast path for clients that aren't in a dup set.
|
||||
return
|
||||
}
|
||||
if c.isDisabled.Get() {
|
||||
// If they're already disabled, no point checking more.
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
ds, ok := s.clients[c.key].(*dupClientSet)
|
||||
if !ok {
|
||||
// It became unduped in between the isDup fast path check above
|
||||
// and the mutex check. Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
if s.dupPolicy == lastWriterIsActive {
|
||||
ds.last = c
|
||||
} else if ds.last == nil {
|
||||
// If we didn't have a primary, let the current
|
||||
// speaker be the primary.
|
||||
ds.last = c
|
||||
}
|
||||
|
||||
if sh := ds.sendHistory; len(sh) != 0 && sh[len(sh)-1] == c {
|
||||
// The client c was the last client to make activity
|
||||
// in this set and it was already recorded. Nothing to
|
||||
// do.
|
||||
return
|
||||
}
|
||||
|
||||
// If we saw this connection send previously, then consider
|
||||
// the group fighting and disable them all.
|
||||
if s.dupPolicy == disableFighters {
|
||||
for _, prior := range ds.sendHistory {
|
||||
if prior == c {
|
||||
ds.ForeachClient(func(c *sclient) {
|
||||
c.isDisabled.Set(true)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append this client to the list of clients who spoke last.
|
||||
ds.sendHistory = append(ds.sendHistory, c)
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error {
|
||||
var nonce [24]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
TokenBucketBytesPerSecond int `json:",omitempty"`
|
||||
TokenBucketBytesBurst int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.NodePublic) error {
|
||||
msg, err := json.Marshal(serverInfo{Version: ProtocolVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgbox := box.Seal(nil, msg, &nonce, clientKey.B32(), s.privateKey.B32())
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(nonce[:]); err != nil {
|
||||
msgbox := s.privateKey.SealTo(clientKey, msg)
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(msgbox); err != nil {
|
||||
@@ -886,7 +1102,7 @@ func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error
|
||||
// recvClientKey reads the frameClientInfo frame from the client (its
|
||||
// proof of identity) upon its initial connection. It should be
|
||||
// considered especially untrusted at this point.
|
||||
func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *clientInfo, err error) {
|
||||
func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.NodePublic, info *clientInfo, err error) {
|
||||
fl, err := readFrameTypeHeader(br, frameClientInfo)
|
||||
if err != nil {
|
||||
return zpub, nil, err
|
||||
@@ -900,21 +1116,17 @@ func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *cl
|
||||
if fl > 256<<10 {
|
||||
return zpub, nil, errors.New("long client info")
|
||||
}
|
||||
if _, err := io.ReadFull(br, clientKey[:]); err != nil {
|
||||
if err := clientKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(br, nonce[:]); err != nil {
|
||||
return zpub, nil, fmt.Errorf("nonce: %v", err)
|
||||
}
|
||||
msgLen := int(fl - minLen)
|
||||
msgLen := int(fl - keyLen)
|
||||
msgbox := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(br, msgbox); err != nil {
|
||||
return zpub, nil, fmt.Errorf("msgbox: %v", err)
|
||||
}
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, (*[32]byte)(&clientKey), s.privateKey.B32())
|
||||
msg, ok := s.privateKey.OpenFrom(clientKey, msgbox)
|
||||
if !ok {
|
||||
return zpub, nil, fmt.Errorf("msgbox: cannot open len=%d with client key %x", msgLen, clientKey[:])
|
||||
return zpub, nil, fmt.Errorf("msgbox: cannot open len=%d with client key %s", msgLen, clientKey)
|
||||
}
|
||||
info = new(clientInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
@@ -923,11 +1135,11 @@ func (s *Server) recvClientKey(br *bufio.Reader) (clientKey key.Public, info *cl
|
||||
return clientKey, info, nil
|
||||
}
|
||||
|
||||
func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Public, contents []byte, err error) {
|
||||
func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.NodePublic, contents []byte, err error) {
|
||||
if frameLen < keyLen {
|
||||
return zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if err := readPublicKey(br, &dstKey); err != nil {
|
||||
if err := dstKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen
|
||||
@@ -948,17 +1160,17 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
return dstKey, contents, nil
|
||||
}
|
||||
|
||||
// zpub is the key.Public zero value.
|
||||
var zpub key.Public
|
||||
// zpub is the key.NodePublic zero value.
|
||||
var zpub key.NodePublic
|
||||
|
||||
func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, dstKey key.Public, contents []byte, err error) {
|
||||
func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, dstKey key.NodePublic, contents []byte, err error) {
|
||||
if frameLen < keyLen*2 {
|
||||
return zpub, zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if _, err := io.ReadFull(br, srcKey[:]); err != nil {
|
||||
if err := srcKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, zpub, nil, err
|
||||
}
|
||||
if _, err := io.ReadFull(br, dstKey[:]); err != nil {
|
||||
if err := dstKey.ReadRawWithoutAllocating(br); err != nil {
|
||||
return zpub, zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen*2
|
||||
@@ -982,17 +1194,19 @@ type sclient struct {
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
key key.NodePublic
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.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
|
||||
isDup syncs.AtomicBool // whether more than 1 sclient for key is connected
|
||||
isDisabled syncs.AtomicBool // whether sends to this peer are disabled due to active/active dups
|
||||
|
||||
// replaceLimiter controls how quickly two connections with
|
||||
// the same client key can kick each other off the server by
|
||||
@@ -1019,14 +1233,14 @@ type sclient struct {
|
||||
// peerConnState represents whether a peer is connected to the server
|
||||
// or not.
|
||||
type peerConnState struct {
|
||||
peer key.Public
|
||||
peer key.NodePublic
|
||||
present bool
|
||||
}
|
||||
|
||||
// pkt is a request to write a data frame to an sclient.
|
||||
type pkt struct {
|
||||
// src is the who's the sender of the packet.
|
||||
src key.Public
|
||||
src key.NodePublic
|
||||
|
||||
// enqueuedAt is when a packet was put onto a queue before it was sent,
|
||||
// and is used for reporting metrics on the duration of packets in the queue.
|
||||
@@ -1171,23 +1385,23 @@ func (c *sclient) sendKeepAlive() error {
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
_, err := c.bw.Write(peer.AppendTo(nil))
|
||||
return err
|
||||
}
|
||||
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.Public) error {
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic) error {
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
_, err := c.bw.Write(peer.AppendTo(nil))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1239,7 +1453,7 @@ func (c *sclient) sendMeshUpdates() error {
|
||||
// DERPv2. The bytes of contents are only valid until this function
|
||||
// returns, do not retain slices.
|
||||
// It does not flush its bufio.Writer.
|
||||
func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error) {
|
||||
defer func() {
|
||||
// Stats update.
|
||||
if err != nil {
|
||||
@@ -1255,14 +1469,13 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
withKey := !srcKey.IsZero()
|
||||
pktLen := len(contents)
|
||||
if withKey {
|
||||
pktLen += len(srcKey)
|
||||
pktLen += srcKey.RawLen()
|
||||
}
|
||||
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
err := writePublicKey(c.bw.bw(), &srcKey)
|
||||
if err != nil {
|
||||
if err := srcKey.WriteRawWithoutAllocating(c.bw.bw()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1272,7 +1485,7 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
|
||||
// AddPacketForwarder registers fwd as a packet forwarder for dst.
|
||||
// fwd must be comparable.
|
||||
func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
func (s *Server) AddPacketForwarder(dst key.NodePublic, fwd PacketForwarder) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if prev, ok := s.clientsMesh[dst]; ok {
|
||||
@@ -1281,7 +1494,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
return
|
||||
}
|
||||
if m, ok := prev.(multiForwarder); ok {
|
||||
if _, ok := m[fwd]; !ok {
|
||||
if _, ok := m[fwd]; ok {
|
||||
// Duplicate registration of same forwarder in set; ignore.
|
||||
return
|
||||
}
|
||||
@@ -1304,7 +1517,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
|
||||
// RemovePacketForwarder removes fwd as a packet forwarder for dst.
|
||||
// fwd must be comparable.
|
||||
func (s *Server) RemovePacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
func (s *Server) RemovePacketForwarder(dst key.NodePublic, fwd PacketForwarder) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
v, ok := s.clientsMesh[dst]
|
||||
@@ -1366,7 +1579,7 @@ func (m multiForwarder) maxVal() (max uint8) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m multiForwarder) ForwardPacket(src, dst key.Public, payload []byte) error {
|
||||
func (m multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte) error {
|
||||
var fwd PacketForwarder
|
||||
var lowest uint8
|
||||
for k, v := range m {
|
||||
@@ -1391,15 +1604,16 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("gauge_memstats_sys0", expvar.Func(func() interface{} { return int64(s.memSys0) }))
|
||||
m.Set("gauge_watchers", s.expVarFunc(func() interface{} { return len(s.watchers) }))
|
||||
m.Set("gauge_current_file_descriptors", expvar.Func(func() interface{} { return metrics.CurrentFDs() }))
|
||||
m.Set("gauge_current_connections", &s.curClients)
|
||||
m.Set("gauge_current_home_connections", &s.curHomeClients)
|
||||
m.Set("gauge_clients_total", expvar.Func(func() interface{} { return len(s.clientsMesh) }))
|
||||
m.Set("gauge_clients_local", expvar.Func(func() interface{} { return len(s.clients) }))
|
||||
m.Set("gauge_clients_remote", expvar.Func(func() interface{} { return len(s.clientsMesh) - len(s.clients) }))
|
||||
m.Set("gauge_current_dup_client_keys", &s.dupClientKeys)
|
||||
m.Set("gauge_current_dup_client_conns", &s.dupClientConns)
|
||||
m.Set("counter_total_dup_client_conns", &s.dupClientConnTotal)
|
||||
m.Set("accepts", &s.accepts)
|
||||
m.Set("clients_replaced", &s.clientsReplaced)
|
||||
m.Set("clients_replace_limited", &s.clientsReplaceLimited)
|
||||
m.Set("gauge_clients_replace_sleeping", &s.clientsReplaceSleeping)
|
||||
m.Set("bytes_received", &s.bytesRecv)
|
||||
m.Set("bytes_sent", &s.bytesSent)
|
||||
m.Set("packets_dropped", &s.packetsDropped)
|
||||
@@ -1465,37 +1679,6 @@ func (s *Server) ConsistencyCheck() error {
|
||||
return errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
// readPublicKey reads key from br.
|
||||
// It is ~4x slower than io.ReadFull(br, key),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If io.ReadFull(br, key) does not cause key to escape, use that instead.
|
||||
func readPublicKey(br *bufio.Reader, key *key.Public) error {
|
||||
// Do io.ReadFull(br, key), but one byte at a time, to avoid allocation.
|
||||
for i := range key {
|
||||
b, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key[i] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePublicKey writes key to bw.
|
||||
// It is ~3x slower than bw.Write(key[:]),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If bw.Write(key[:]) does not cause key to escape, use that instead.
|
||||
func writePublicKey(bw *bufio.Writer, key *key.Public) error {
|
||||
// Do bw.Write(key[:]), but one byte at a time to avoid allocation.
|
||||
for _, b := range key {
|
||||
err := bw.WriteByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const minTimeBetweenLogs = 2 * time.Second
|
||||
|
||||
// BytesSentRecv records the number of bytes that have been sent since the last traffic check
|
||||
@@ -1504,7 +1687,7 @@ type BytesSentRecv struct {
|
||||
Sent uint64
|
||||
Recv uint64
|
||||
// Key is the public key of the client which sent/received these bytes.
|
||||
Key key.Public
|
||||
Key key.NodePublic
|
||||
}
|
||||
|
||||
// parseSSOutput parses the output from the specific call to ss in ServeDebugTraffic.
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -23,20 +22,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newPrivateKey(tb testing.TB) (k key.Private) {
|
||||
tb.Helper()
|
||||
if _, err := crand.Read(k[:]); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
`{"Version":5,"MeshKey":"abc"}`,
|
||||
@@ -54,15 +46,15 @@ func TestClientInfoUnmarshal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
|
||||
const numClients = 3
|
||||
var clientPrivateKeys []key.Private
|
||||
var clientKeys []key.Public
|
||||
var clientPrivateKeys []key.NodePrivate
|
||||
var clientKeys []key.NodePublic
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := newPrivateKey(t)
|
||||
priv := key.NewNode()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
@@ -225,7 +217,7 @@ func TestSendRecv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendFreeze(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
s.WriteTimeout = 100 * time.Millisecond
|
||||
@@ -238,7 +230,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
// Then cathy stops processing messsages.
|
||||
// That should not interfere with alice talking to bob.
|
||||
|
||||
newClient := func(name string, k key.Private) (c *Client, clientConn nettest.Conn) {
|
||||
newClient := func(name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
|
||||
t.Helper()
|
||||
c1, c2 := nettest.NewConn(name, 1024)
|
||||
go s.Accept(c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
|
||||
@@ -252,13 +244,13 @@ func TestSendFreeze(t *testing.T) {
|
||||
return c, c2
|
||||
}
|
||||
|
||||
aliceKey := newPrivateKey(t)
|
||||
aliceKey := key.NewNode()
|
||||
aliceClient, aliceConn := newClient("alice", aliceKey)
|
||||
|
||||
bobKey := newPrivateKey(t)
|
||||
bobKey := key.NewNode()
|
||||
bobClient, bobConn := newClient("bob", bobKey)
|
||||
|
||||
cathyKey := newPrivateKey(t)
|
||||
cathyKey := key.NewNode()
|
||||
cathyClient, cathyConn := newClient("cathy", cathyKey)
|
||||
|
||||
var (
|
||||
@@ -403,8 +395,11 @@ func TestSendFreeze(t *testing.T) {
|
||||
t.Logf("TEST COMPLETE, cancelling sender")
|
||||
cancel()
|
||||
t.Logf("closing connections")
|
||||
aliceConn.Close()
|
||||
// Close bob before alice.
|
||||
// Starting with alice can cause a PeerGoneMessage to reach
|
||||
// bob before bob is closed, causing a test flake (issue 2668).
|
||||
bobConn.Close()
|
||||
aliceConn.Close()
|
||||
cathyConn.Close()
|
||||
|
||||
for i := 0; i < cap(errCh); i++ {
|
||||
@@ -424,7 +419,7 @@ type testServer struct {
|
||||
logf logger.Logf
|
||||
|
||||
mu sync.Mutex
|
||||
pubName map[key.Public]string
|
||||
pubName map[key.NodePublic]string
|
||||
clients map[*testClient]bool
|
||||
}
|
||||
|
||||
@@ -434,14 +429,14 @@ func (ts *testServer) addTestClient(c *testClient) {
|
||||
ts.clients[c] = true
|
||||
}
|
||||
|
||||
func (ts *testServer) addKeyName(k key.Public, name string) {
|
||||
func (ts *testServer) addKeyName(k key.NodePublic, name string) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
ts.pubName[k] = name
|
||||
ts.logf("test adding named key %q for %x", name, k)
|
||||
}
|
||||
|
||||
func (ts *testServer) keyName(k key.Public) string {
|
||||
func (ts *testServer) keyName(k key.NodePublic) string {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
if name, ok := ts.pubName[k]; ok {
|
||||
@@ -462,7 +457,7 @@ func (ts *testServer) close(t *testing.T) error {
|
||||
func newTestServer(t *testing.T) *testServer {
|
||||
t.Helper()
|
||||
logf := logger.WithPrefix(t.Logf, "derp-server: ")
|
||||
s := NewServer(newPrivateKey(t), logf)
|
||||
s := NewServer(key.NewNode(), logf)
|
||||
s.SetMeshKey("mesh-key")
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -488,7 +483,7 @@ func newTestServer(t *testing.T) *testServer {
|
||||
ln: ln,
|
||||
logf: logf,
|
||||
clients: map[*testClient]bool{},
|
||||
pubName: map[key.Public]string{},
|
||||
pubName: map[key.NodePublic]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,20 +491,20 @@ type testClient struct {
|
||||
name string
|
||||
c *Client
|
||||
nc net.Conn
|
||||
pub key.Public
|
||||
pub key.NodePublic
|
||||
ts *testServer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.Private, logger.Logf) (*Client, error)) *testClient {
|
||||
func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.NodePrivate, logger.Logf) (*Client, error)) *testClient {
|
||||
t.Helper()
|
||||
nc, err := net.Dial("tcp", ts.ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := newPrivateKey(t)
|
||||
ts.addKeyName(key.Public(), name)
|
||||
c, err := newClient(nc, key, logger.WithPrefix(t.Logf, "client-"+name+": "))
|
||||
k := key.NewNode()
|
||||
ts.addKeyName(k.Public(), name)
|
||||
c, err := newClient(nc, k, logger.WithPrefix(t.Logf, "client-"+name+": "))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -518,14 +513,14 @@ func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net
|
||||
nc: nc,
|
||||
c: c,
|
||||
ts: ts,
|
||||
pub: key.Public(),
|
||||
pub: k.Public(),
|
||||
}
|
||||
ts.addTestClient(tc)
|
||||
return tc
|
||||
}
|
||||
|
||||
func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf)
|
||||
if err != nil {
|
||||
@@ -538,7 +533,7 @@ func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
}
|
||||
|
||||
func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf, MeshKey("mesh-key"))
|
||||
if err != nil {
|
||||
@@ -552,9 +547,9 @@ func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
|
||||
t.Helper()
|
||||
want := map[key.Public]bool{}
|
||||
want := map[key.NodePublic]bool{}
|
||||
for _, k := range peers {
|
||||
want[k] = true
|
||||
}
|
||||
@@ -566,7 +561,7 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerPresentMessage:
|
||||
got := key.Public(m)
|
||||
got := key.NodePublic(m)
|
||||
if !want[got] {
|
||||
t.Fatalf("got peer present for %v; want present for %v", tc.ts.keyName(got), logger.ArgWriter(func(bw *bufio.Writer) {
|
||||
for _, pub := range peers {
|
||||
@@ -584,7 +579,7 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *testClient) wantGone(t *testing.T, peer key.Public) {
|
||||
func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
|
||||
t.Helper()
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
@@ -592,7 +587,7 @@ func (tc *testClient) wantGone(t *testing.T, peer key.Public) {
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerGoneMessage:
|
||||
got := key.Public(m)
|
||||
got := key.NodePublic(m)
|
||||
if peer != got {
|
||||
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
|
||||
}
|
||||
@@ -651,21 +646,24 @@ func TestWatch(t *testing.T) {
|
||||
|
||||
type testFwd int
|
||||
|
||||
func (testFwd) ForwardPacket(key.Public, key.Public, []byte) error { panic("not called in tests") }
|
||||
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
|
||||
panic("not called in tests")
|
||||
}
|
||||
|
||||
func pubAll(b byte) (ret key.Public) {
|
||||
for i := range ret {
|
||||
ret[i] = b
|
||||
func pubAll(b byte) (ret key.NodePublic) {
|
||||
var bs [32]byte
|
||||
for i := range bs {
|
||||
bs[i] = b
|
||||
}
|
||||
return
|
||||
return key.NodePublicFromRaw32(mem.B(bs[:]))
|
||||
}
|
||||
|
||||
func TestForwarderRegistration(t *testing.T) {
|
||||
s := &Server{
|
||||
clients: make(map[key.Public]*sclient),
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
clients: make(map[key.NodePublic]clientSet),
|
||||
clientsMesh: map[key.NodePublic]PacketForwarder{},
|
||||
}
|
||||
want := func(want map[key.Public]PacketForwarder) {
|
||||
want := func(want map[key.NodePublic]PacketForwarder) {
|
||||
t.Helper()
|
||||
if got := s.clientsMesh; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("mismatch\n got: %v\nwant: %v\n", got, want)
|
||||
@@ -684,35 +682,36 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
|
||||
s.AddPacketForwarder(u1, testFwd(1))
|
||||
s.AddPacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Verify a remove of non-registered forwarder is no-op.
|
||||
s.RemovePacketForwarder(u2, testFwd(999))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Verify a remove of non-registered user is no-op.
|
||||
s.RemovePacketForwarder(u3, testFwd(1))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Actual removal.
|
||||
s.RemovePacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
})
|
||||
|
||||
// Adding a dup for a user.
|
||||
wantCounter(&s.multiForwarderCreated, 0)
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
@@ -722,7 +721,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
|
||||
// Removing a forwarder in a multi set that doesn't exist; does nothing.
|
||||
s.RemovePacketForwarder(u1, testFwd(55))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
@@ -733,7 +732,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
// from being a multiForwarder.
|
||||
wantCounter(&s.multiForwarderDeleted, 0)
|
||||
s.RemovePacketForwarder(u1, testFwd(1))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(100),
|
||||
})
|
||||
wantCounter(&s.multiForwarderDeleted, 1)
|
||||
@@ -744,39 +743,39 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
key: u1,
|
||||
logf: logger.Discard,
|
||||
}
|
||||
s.clients[u1] = u1c
|
||||
s.clients[u1] = singleClient{u1c}
|
||||
s.RemovePacketForwarder(u1, testFwd(100))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
|
||||
// But once that client disconnects, it should go away.
|
||||
s.unregisterClient(u1c)
|
||||
want(map[key.Public]PacketForwarder{})
|
||||
want(map[key.NodePublic]PacketForwarder{})
|
||||
|
||||
// But if it already has a forwarder, it's not removed.
|
||||
s.AddPacketForwarder(u1, testFwd(2))
|
||||
s.unregisterClient(u1c)
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(2),
|
||||
})
|
||||
|
||||
// Now pretend u1 was already connected locally (so clientsMesh[u1] is nil), and then we heard
|
||||
// that they're also connected to a peer of ours. That sholdn't transition the forwarder
|
||||
// from nil to the new one, not a multiForwarder.
|
||||
s.clients[u1] = u1c
|
||||
s.clients[u1] = singleClient{u1c}
|
||||
s.clientsMesh[u1] = nil
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
s.AddPacketForwarder(u1, testFwd(3))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: testFwd(3),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := newPrivateKey(t)
|
||||
priv := key.NewNode()
|
||||
pub := priv.Public()
|
||||
s := NewServer(priv, t.Logf)
|
||||
|
||||
@@ -788,7 +787,7 @@ func TestMetaCert(t *testing.T) {
|
||||
if fmt.Sprint(cert.SerialNumber) != fmt.Sprint(ProtocolVersion) {
|
||||
t.Errorf("serial = %v; want %v", cert.SerialNumber, ProtocolVersion)
|
||||
}
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%x", pub[:]); g != w {
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%s", pub.UntypedHexString()); g != w {
|
||||
t.Errorf("CommonName = %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
@@ -813,6 +812,33 @@ func TestClientRecv(t *testing.T) {
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
{
|
||||
name: "health_bad",
|
||||
input: []byte{
|
||||
byte(frameHealth), 0, 0, 0, 3,
|
||||
byte('B'), byte('A'), byte('D'),
|
||||
},
|
||||
want: HealthMessage{Problem: "BAD"},
|
||||
},
|
||||
{
|
||||
name: "health_ok",
|
||||
input: []byte{
|
||||
byte(frameHealth), 0, 0, 0, 0,
|
||||
},
|
||||
want: HealthMessage{},
|
||||
},
|
||||
{
|
||||
name: "server_restarting",
|
||||
input: []byte{
|
||||
byte(frameRestarting), 0, 0, 0, 8,
|
||||
0, 0, 0, 1,
|
||||
0, 0, 0, 2,
|
||||
},
|
||||
want: ServerRestartingMessage{
|
||||
ReconnectIn: 1 * time.Millisecond,
|
||||
TryFor: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -850,125 +876,248 @@ func TestClientSendPong(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestServerReplaceClients(t *testing.T) {
|
||||
defer func() {
|
||||
timeSleep = time.Sleep
|
||||
timeNow = time.Now
|
||||
}()
|
||||
func TestServerDupClients(t *testing.T) {
|
||||
serverPriv := key.NewNode()
|
||||
var s *Server
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
now = time.Unix(123, 0)
|
||||
sleeps int
|
||||
slept time.Duration
|
||||
)
|
||||
timeSleep = func(d time.Duration) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
sleeps++
|
||||
slept += d
|
||||
now = now.Add(d)
|
||||
}
|
||||
timeNow = func() time.Time {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return now
|
||||
}
|
||||
clientPriv := key.NewNode()
|
||||
clientPub := clientPriv.Public()
|
||||
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
var logger logger.Logf = logger.Discard
|
||||
const debug = false
|
||||
if debug {
|
||||
logger = t.Logf
|
||||
}
|
||||
var c1, c2, c3 *sclient
|
||||
var clientName map[*sclient]string
|
||||
|
||||
s := NewServer(serverPrivateKey, logger)
|
||||
defer s.Close()
|
||||
|
||||
priv := newPrivateKey(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
connNum := 0
|
||||
connect := func() *Client {
|
||||
connNum++
|
||||
cout, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// run starts a new test case and resets clients back to their zero values.
|
||||
run := func(name string, dupPolicy dupPolicy, f func(t *testing.T)) {
|
||||
s = NewServer(serverPriv, t.Logf)
|
||||
s.dupPolicy = dupPolicy
|
||||
c1 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c1: ")}
|
||||
c2 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c2: ")}
|
||||
c3 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c3: ")}
|
||||
clientName = map[*sclient]string{
|
||||
c1: "c1",
|
||||
c2: "c2",
|
||||
c3: "c3",
|
||||
}
|
||||
|
||||
cin, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
|
||||
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", connNum))
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
|
||||
c, err := NewClient(priv, cout, brw, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", connNum, err)
|
||||
}
|
||||
return c
|
||||
t.Run(name, f)
|
||||
}
|
||||
|
||||
wantVar := func(v *expvar.Int, want int64) {
|
||||
runBothWays := func(name string, f func(t *testing.T)) {
|
||||
run(name+"_disablefighters", disableFighters, f)
|
||||
run(name+"_lastwriteractive", lastWriterIsActive, f)
|
||||
}
|
||||
wantSingleClient := func(t *testing.T, want *sclient) {
|
||||
t.Helper()
|
||||
if got := v.Value(); got != want {
|
||||
t.Errorf("got %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
wantClosed := func(c *Client) {
|
||||
t.Helper()
|
||||
for {
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Logf("got expected error: %v", err)
|
||||
switch s := s.clients[want.key].(type) {
|
||||
case singleClient:
|
||||
if s.c != want {
|
||||
t.Error("wrong single client")
|
||||
return
|
||||
}
|
||||
switch m.(type) {
|
||||
case ServerInfoMessage:
|
||||
continue
|
||||
default:
|
||||
t.Fatalf("client got %T; wanted an error", m)
|
||||
if want.isDup.Get() {
|
||||
t.Errorf("unexpected isDup on singleClient")
|
||||
}
|
||||
if want.isDisabled.Get() {
|
||||
t.Errorf("unexpected isDisabled on singleClient")
|
||||
}
|
||||
case nil:
|
||||
t.Error("no clients for key")
|
||||
case *dupClientSet:
|
||||
t.Error("unexpected multiple clients for key")
|
||||
}
|
||||
}
|
||||
wantNoClient := func(t *testing.T) {
|
||||
t.Helper()
|
||||
switch s := s.clients[clientPub].(type) {
|
||||
case nil:
|
||||
// Good.
|
||||
return
|
||||
default:
|
||||
t.Errorf("got %T; want empty", s)
|
||||
}
|
||||
}
|
||||
wantDupSet := func(t *testing.T) *dupClientSet {
|
||||
t.Helper()
|
||||
switch s := s.clients[clientPub].(type) {
|
||||
case *dupClientSet:
|
||||
return s
|
||||
default:
|
||||
t.Fatalf("wanted dup set; got %T", s)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
wantActive := func(t *testing.T, want *sclient) {
|
||||
t.Helper()
|
||||
set, ok := s.clients[clientPub]
|
||||
if !ok {
|
||||
t.Error("no set for key")
|
||||
return
|
||||
}
|
||||
got := set.ActiveClient()
|
||||
if got != want {
|
||||
t.Errorf("active client = %q; want %q", clientName[got], clientName[want])
|
||||
}
|
||||
}
|
||||
checkDup := func(t *testing.T, c *sclient, want bool) {
|
||||
t.Helper()
|
||||
if got := c.isDup.Get(); got != want {
|
||||
t.Errorf("client %q isDup = %v; want %v", clientName[c], got, want)
|
||||
}
|
||||
}
|
||||
checkDisabled := func(t *testing.T, c *sclient, want bool) {
|
||||
t.Helper()
|
||||
if got := c.isDisabled.Get(); got != want {
|
||||
t.Errorf("client %q isDisabled = %v; want %v", clientName[c], got, want)
|
||||
}
|
||||
}
|
||||
wantDupConns := func(t *testing.T, want int) {
|
||||
t.Helper()
|
||||
if got := s.dupClientConns.Value(); got != int64(want) {
|
||||
t.Errorf("dupClientConns = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
wantDupKeys := func(t *testing.T, want int) {
|
||||
t.Helper()
|
||||
if got := s.dupClientKeys.Value(); got != int64(want) {
|
||||
t.Errorf("dupClientKeys = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
c1 := connect()
|
||||
waitConnect(t, c1)
|
||||
c2 := connect()
|
||||
waitConnect(t, c2)
|
||||
wantVar(&s.clientsReplaced, 1)
|
||||
wantClosed(c1)
|
||||
// Common case: a single client comes and goes, with no dups.
|
||||
runBothWays("one_comes_and_goes", func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
s.registerClient(c1)
|
||||
wantSingleClient(t, c1)
|
||||
s.unregisterClient(c1)
|
||||
wantNoClient(t)
|
||||
})
|
||||
|
||||
for i := 0; i < 100+5; i++ {
|
||||
c := connect()
|
||||
defer c.nc.Close()
|
||||
if s.clientsReplaceLimited.Value() == 0 && i < 90 {
|
||||
continue
|
||||
}
|
||||
t.Logf("for %d: replaced=%d, limited=%d, sleeping=%d", i,
|
||||
s.clientsReplaced.Value(),
|
||||
s.clientsReplaceLimited.Value(),
|
||||
s.clientsReplaceSleeping.Value(),
|
||||
)
|
||||
}
|
||||
// A still somewhat common case: a single client was
|
||||
// connected and then their wifi dies or laptop closes
|
||||
// or they switch networks and connect from a
|
||||
// different network. They have two connections but
|
||||
// it's not very bad. Only their new one is
|
||||
// active. The last one, being dead, doesn't send and
|
||||
// thus the new one doesn't get disabled.
|
||||
runBothWays("small_overlap_replacement", func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
s.registerClient(c1)
|
||||
wantSingleClient(t, c1)
|
||||
wantActive(t, c1)
|
||||
wantDupKeys(t, 0)
|
||||
wantDupKeys(t, 0)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if sleeps == 0 {
|
||||
t.Errorf("no sleeps")
|
||||
}
|
||||
if slept == 0 {
|
||||
t.Errorf("total sleep duration was 0")
|
||||
}
|
||||
s.registerClient(c2) // wifi dies; c2 replacement connects
|
||||
wantDupSet(t)
|
||||
wantDupConns(t, 2)
|
||||
wantDupKeys(t, 1)
|
||||
checkDup(t, c1, true)
|
||||
checkDup(t, c2, true)
|
||||
checkDisabled(t, c1, false)
|
||||
checkDisabled(t, c2, false)
|
||||
wantActive(t, c2) // sends go to the replacement
|
||||
|
||||
s.unregisterClient(c1) // c1 finally times out
|
||||
wantSingleClient(t, c2)
|
||||
checkDup(t, c2, false) // c2 is longer a dup
|
||||
wantActive(t, c2)
|
||||
wantDupConns(t, 0)
|
||||
wantDupKeys(t, 0)
|
||||
})
|
||||
|
||||
// Key cloning situation with concurrent clients, both trying
|
||||
// to write.
|
||||
run("concurrent_dups_get_disabled", disableFighters, func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
s.registerClient(c1)
|
||||
wantSingleClient(t, c1)
|
||||
wantActive(t, c1)
|
||||
s.registerClient(c2)
|
||||
wantDupSet(t)
|
||||
wantDupKeys(t, 1)
|
||||
wantDupConns(t, 2)
|
||||
wantActive(t, c2)
|
||||
checkDup(t, c1, true)
|
||||
checkDup(t, c2, true)
|
||||
checkDisabled(t, c1, false)
|
||||
checkDisabled(t, c2, false)
|
||||
|
||||
s.noteClientActivity(c2)
|
||||
checkDisabled(t, c1, false)
|
||||
checkDisabled(t, c2, false)
|
||||
s.noteClientActivity(c1)
|
||||
checkDisabled(t, c1, true)
|
||||
checkDisabled(t, c2, true)
|
||||
wantActive(t, nil)
|
||||
|
||||
s.registerClient(c3)
|
||||
wantActive(t, c3)
|
||||
checkDisabled(t, c3, false)
|
||||
wantDupKeys(t, 1)
|
||||
wantDupConns(t, 3)
|
||||
|
||||
s.unregisterClient(c3)
|
||||
wantActive(t, nil)
|
||||
wantDupKeys(t, 1)
|
||||
wantDupConns(t, 2)
|
||||
|
||||
s.unregisterClient(c2)
|
||||
wantSingleClient(t, c1)
|
||||
wantDupKeys(t, 0)
|
||||
wantDupConns(t, 0)
|
||||
})
|
||||
|
||||
// Key cloning with an A->B->C->A series instead.
|
||||
run("concurrent_dups_three_parties", disableFighters, func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
s.registerClient(c1)
|
||||
s.registerClient(c2)
|
||||
s.registerClient(c3)
|
||||
s.noteClientActivity(c1)
|
||||
checkDisabled(t, c1, true)
|
||||
checkDisabled(t, c2, true)
|
||||
checkDisabled(t, c3, true)
|
||||
wantActive(t, nil)
|
||||
})
|
||||
|
||||
run("activity_promotes_primary_when_nil", disableFighters, func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
|
||||
// Last registered client is the active one...
|
||||
s.registerClient(c1)
|
||||
wantActive(t, c1)
|
||||
s.registerClient(c2)
|
||||
wantActive(t, c2)
|
||||
s.registerClient(c3)
|
||||
s.noteClientActivity(c2)
|
||||
wantActive(t, c3)
|
||||
|
||||
// But if the last one goes away, the one with the
|
||||
// most recent activity wins.
|
||||
s.unregisterClient(c3)
|
||||
wantActive(t, c2)
|
||||
})
|
||||
|
||||
run("concurrent_dups_three_parties_last_writer", lastWriterIsActive, func(t *testing.T) {
|
||||
wantNoClient(t)
|
||||
|
||||
s.registerClient(c1)
|
||||
wantActive(t, c1)
|
||||
s.registerClient(c2)
|
||||
wantActive(t, c2)
|
||||
|
||||
s.noteClientActivity(c1)
|
||||
checkDisabled(t, c1, false)
|
||||
checkDisabled(t, c2, false)
|
||||
wantActive(t, c1)
|
||||
|
||||
s.noteClientActivity(c2)
|
||||
checkDisabled(t, c1, false)
|
||||
checkDisabled(t, c2, false)
|
||||
wantActive(t, c2)
|
||||
|
||||
s.unregisterClient(c2)
|
||||
checkDisabled(t, c1, false)
|
||||
wantActive(t, c1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLimiter(t *testing.T) {
|
||||
@@ -987,12 +1136,12 @@ func BenchmarkSendRecv(b *testing.B) {
|
||||
}
|
||||
|
||||
func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
serverPrivateKey := newPrivateKey(b)
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := NewServer(serverPrivateKey, logger.Discard)
|
||||
defer s.Close()
|
||||
|
||||
key := newPrivateKey(b)
|
||||
clientKey := key.Public()
|
||||
k := key.NewNode()
|
||||
clientKey := k.Public()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -1016,7 +1165,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
go s.Accept(connIn, brwServer, "test-client")
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
|
||||
client, err := NewClient(key, connOut, brw, logger.Discard)
|
||||
client, err := NewClient(k, connOut, brw, logger.Discard)
|
||||
if err != nil {
|
||||
b.Fatalf("client: %v", err)
|
||||
}
|
||||
@@ -1090,3 +1239,80 @@ func TestParseSSOutput(t *testing.T) {
|
||||
t.Errorf("parseSSOutput expected non-empty map")
|
||||
}
|
||||
}
|
||||
|
||||
type countWriter struct {
|
||||
mu sync.Mutex
|
||||
writes int
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func (w *countWriter) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.writes++
|
||||
w.bytes += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *countWriter) Stats() (writes int, bytes int64) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.writes, w.bytes
|
||||
}
|
||||
|
||||
func (w *countWriter) ResetStats() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.writes, w.bytes = 0, 0
|
||||
}
|
||||
|
||||
func TestClientSendRateLimiting(t *testing.T) {
|
||||
cw := new(countWriter)
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(cw),
|
||||
}
|
||||
c.setSendRateLimiter(ServerInfoMessage{})
|
||||
|
||||
pkt := make([]byte, 1000)
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writes1, bytes1 := cw.Stats()
|
||||
if writes1 != 1 {
|
||||
t.Errorf("writes = %v, want 1", writes1)
|
||||
}
|
||||
|
||||
// Flood should all succeed.
|
||||
cw.ResetStats()
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writes1K, bytes1K := cw.Stats()
|
||||
if writes1K != 1000 {
|
||||
t.Logf("writes = %v; want 1000", writes1K)
|
||||
}
|
||||
if got, want := bytes1K, bytes1*1000; got != want {
|
||||
t.Logf("bytes = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
// Set a rate limiter
|
||||
cw.ResetStats()
|
||||
c.setSendRateLimiter(ServerInfoMessage{
|
||||
TokenBucketBytesPerSecond: 1,
|
||||
TokenBucketBytesBurst: int(bytes1 * 2),
|
||||
})
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writesLimited, bytesLimited := cw.Stats()
|
||||
if writesLimited == 0 || writesLimited == writes1K {
|
||||
t.Errorf("limited conn's write count = %v; want non-zero, less than 1k", writesLimited)
|
||||
}
|
||||
if bytesLimited < bytes1*2 || bytesLimited >= bytes1K {
|
||||
t.Errorf("limited conn's bytes count = %v; want >=%v, <%v", bytesLimited, bytes1K*2, bytes1K)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -52,7 +53,7 @@ type Client struct {
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
privateKey key.Private
|
||||
privateKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
@@ -70,12 +71,12 @@ type Client struct {
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.Public
|
||||
serverPubKey key.NodePublic
|
||||
}
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewRegionClient(privateKey key.Private, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
@@ -95,7 +96,7 @@ func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
|
||||
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewClient(privateKey key.Private, serverURL string, logf logger.Logf) (*Client, error) {
|
||||
func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf) (*Client, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derphttp.NewClient: %v", err)
|
||||
@@ -126,14 +127,14 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
//
|
||||
// It only returns a non-zero value once a connection has succeeded
|
||||
// from an earlier call.
|
||||
func (c *Client) ServerPublicKey() key.Public {
|
||||
func (c *Client) ServerPublicKey() key.NodePublic {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.serverPubKey
|
||||
}
|
||||
|
||||
// SelfPublicKey returns our own public key.
|
||||
func (c *Client) SelfPublicKey() key.Public {
|
||||
func (c *Client) SelfPublicKey() key.NodePublic {
|
||||
return c.privateKey.Public()
|
||||
}
|
||||
|
||||
@@ -179,6 +180,20 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// dialWebsocketFunc is non-nil (set by websocket.go's init) when compiled in.
|
||||
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
|
||||
|
||||
func useWebsockets() bool {
|
||||
if runtime.GOOS == "js" {
|
||||
return true
|
||||
}
|
||||
if dialWebsocketFunc != nil {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DERP_WS_CLIENT"))
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, connGen int, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -231,10 +246,44 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}()
|
||||
|
||||
var node *tailcfg.DERPNode // nil when using c.url to dial
|
||||
if c.url != nil {
|
||||
switch {
|
||||
case useWebsockets():
|
||||
var urlStr string
|
||||
if c.url != nil {
|
||||
urlStr = c.url.String()
|
||||
} else {
|
||||
urlStr = c.urlString(reg.Nodes[0])
|
||||
}
|
||||
c.logf("%s: connecting websocket to %v", caller, urlStr)
|
||||
conn, err := dialWebsocketFunc(ctx, urlStr)
|
||||
if err != nil {
|
||||
c.logf("%s: websocket to %v error: %v", caller, urlStr, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
derpClient, err := derp.NewClient(c.privateKey, conn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
derp.IsProber(c.IsProber),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if c.preferred {
|
||||
if err := derpClient.NotePreferred(true); err != nil {
|
||||
go conn.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
case c.url != nil:
|
||||
c.logf("%s: connecting to %v", caller, c.url)
|
||||
tcpConn, err = c.dialURL(ctx)
|
||||
} else {
|
||||
default:
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
}
|
||||
@@ -266,8 +315,8 @@ 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.Public // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
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
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
@@ -430,19 +479,14 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
|
||||
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig)
|
||||
if node != nil {
|
||||
tlsConf.InsecureSkipVerify = node.InsecureForTests
|
||||
if node.InsecureForTests {
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
tlsConf.VerifyConnection = nil
|
||||
}
|
||||
if node.CertName != "" {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
}
|
||||
if n := os.Getenv("SSLKEYLOGFILE"); n != "" {
|
||||
f, err := os.OpenFile(n, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("WARNING: writing to SSLKEYLOGFILE %v", n)
|
||||
tlsConf.KeyLogWriter = f
|
||||
}
|
||||
return tls.Client(nc, tlsConf)
|
||||
}
|
||||
|
||||
@@ -643,7 +687,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -654,7 +698,7 @@ func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -735,7 +779,7 @@ func (c *Client) WatchConnectionChanges() error {
|
||||
// ClosePeer asks the server to close target's TCP connection.
|
||||
//
|
||||
// Only trusted connections (using MeshKey) are allowed to use this.
|
||||
func (c *Client) ClosePeer(target key.Public) error {
|
||||
func (c *Client) ClosePeer(target key.NodePublic) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ClosePeer")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -819,15 +863,15 @@ func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.Public, serverProtoVersion int) {
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.NodePublic, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") {
|
||||
var err error
|
||||
serverPub, err = key.NewPublicFromHexMem(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
serverPub, err = key.ParseNodePublicUntyped(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
}
|
||||
}
|
||||
return key.Public{}, 0
|
||||
return key.NodePublic{}, 0
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
@@ -20,10 +21,15 @@ const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Printf("Weird upgrade: %q", up)
|
||||
}
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
@@ -45,9 +51,9 @@ func Handler(s *derp.Server) http.Handler {
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %x\r\n\r\n",
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey[:])
|
||||
pubKey.UntypedHexString())
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
)
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := key.NewPrivate()
|
||||
serverPrivateKey := key.NewNode()
|
||||
|
||||
const numClients = 3
|
||||
var clientPrivateKeys []key.Private
|
||||
var clientKeys []key.Public
|
||||
var clientPrivateKeys []key.NodePrivate
|
||||
var clientKeys []key.NodePublic
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := key.NewPrivate()
|
||||
priv := key.NewNode()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
//
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
|
||||
// be closed, and c itself needs to be closed.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add, remove func(key.NodePublic)) {
|
||||
if infoLogf == nil {
|
||||
infoLogf = logger.Discard
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
const statusInterval = 10 * time.Second
|
||||
var (
|
||||
mu sync.Mutex
|
||||
present = map[key.Public]bool{}
|
||||
present = map[key.NodePublic]bool{}
|
||||
loggedConnected = false
|
||||
)
|
||||
clear := func() {
|
||||
@@ -49,7 +49,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
for k := range present {
|
||||
remove(k)
|
||||
}
|
||||
present = map[key.Public]bool{}
|
||||
present = map[key.NodePublic]bool{}
|
||||
}
|
||||
lastConnGen := 0
|
||||
lastStatus := time.Now()
|
||||
@@ -69,7 +69,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
updatePeer := func(k key.Public, isPresent bool) {
|
||||
updatePeer := func(k key.NodePublic, isPresent bool) {
|
||||
if isPresent {
|
||||
add(k)
|
||||
} else {
|
||||
@@ -127,9 +127,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(key.Public(m), true)
|
||||
updatePeer(key.NodePublic(m), true)
|
||||
case derp.PeerGoneMessage:
|
||||
updatePeer(key.Public(m), false)
|
||||
updatePeer(key.NodePublic(m), false)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
33
derp/derphttp/websocket.go
Normal file
33
derp/derphttp/websocket.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || js
|
||||
// +build linux js
|
||||
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dialWebsocketFunc = dialWebsocket
|
||||
}
|
||||
|
||||
func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
c, res, err := websocket.Dial(ctx, urlStr, &websocket.DialOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket Dial: %v, %+v", err, res)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
return wsconn.New(c), nil
|
||||
}
|
||||
@@ -18,11 +18,12 @@ func _() {
|
||||
_ = x[dropReasonQueueHead-3]
|
||||
_ = x[dropReasonQueueTail-4]
|
||||
_ = x[dropReasonWriteError-5]
|
||||
_ = x[dropReasonDupClient-6]
|
||||
}
|
||||
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteError"
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClient"
|
||||
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59}
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68}
|
||||
|
||||
func (i dropReason) String() string {
|
||||
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
|
||||
|
||||
104
derp/wsconn/wsconn.go
Normal file
104
derp/wsconn/wsconn.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 wsconn contains an adapter type that turns
|
||||
// a websocket connection into a net.Conn.
|
||||
package wsconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// New returns a net.Conn wrapper around c,
|
||||
// using c to send and receive binary messages with
|
||||
// chunks of bytes with no defined framing, effectively
|
||||
// discarding all WebSocket-level message framing.
|
||||
func New(c *websocket.Conn) net.Conn {
|
||||
return &websocketConn{c: c}
|
||||
}
|
||||
|
||||
// websocketConn implements derp.Conn around a *websocket.Conn,
|
||||
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
|
||||
// frame/message boundaries.
|
||||
type websocketConn struct {
|
||||
c *websocket.Conn
|
||||
|
||||
// rextra are extra bytes owned by the reader.
|
||||
rextra []byte
|
||||
|
||||
mu sync.Mutex
|
||||
rdeadline time.Time
|
||||
cancelRead context.CancelFunc
|
||||
}
|
||||
|
||||
func (wc *websocketConn) LocalAddr() net.Addr { return addr{} }
|
||||
func (wc *websocketConn) RemoteAddr() net.Addr { return addr{} }
|
||||
|
||||
type addr struct{}
|
||||
|
||||
func (addr) Network() string { return "websocket" }
|
||||
func (addr) String() string { return "websocket" }
|
||||
|
||||
func (wc *websocketConn) Read(p []byte) (n int, err error) {
|
||||
// Drain any leftover from previously.
|
||||
n = copy(p, wc.rextra)
|
||||
if n > 0 {
|
||||
wc.rextra = wc.rextra[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
||||
wc.mu.Lock()
|
||||
if dl := wc.rdeadline; !dl.IsZero() {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), wc.rdeadline)
|
||||
} else {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(30*24*time.Hour))
|
||||
wc.rdeadline = time.Time{}
|
||||
}
|
||||
wc.cancelRead = cancel
|
||||
wc.mu.Unlock()
|
||||
defer cancel()
|
||||
|
||||
_, buf, err := wc.c.Read(ctx)
|
||||
n = copy(p, buf)
|
||||
wc.rextra = buf[n:]
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Write(p []byte) (n int, err error) {
|
||||
err = wc.c.Write(context.Background(), websocket.MessageBinary, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Close() error { return wc.c.Close(websocket.StatusNormalClosure, "close") }
|
||||
|
||||
func (wc *websocketConn) SetDeadline(t time.Time) error {
|
||||
wc.SetReadDeadline(t)
|
||||
wc.SetWriteDeadline(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
if !t.IsZero() && (wc.rdeadline.IsZero() || t.Before(wc.rdeadline)) && wc.cancelRead != nil {
|
||||
wc.cancelRead()
|
||||
}
|
||||
wc.rdeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"net"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// Magic is the 6 byte header of all discovery messages.
|
||||
@@ -57,6 +58,16 @@ func LooksLikeDiscoWrapper(p []byte) bool {
|
||||
return string(p[:len(Magic)]) == Magic
|
||||
}
|
||||
|
||||
// Source returns the slice of p that represents the
|
||||
// disco public key source, and whether p looks like
|
||||
// a disco message.
|
||||
func Source(p []byte) (src []byte, ok bool) {
|
||||
if !LooksLikeDiscoWrapper(p) {
|
||||
return nil, false
|
||||
}
|
||||
return p[len(Magic):][:keyLen], true
|
||||
}
|
||||
|
||||
// Parse parses the encrypted part of the message from inside the
|
||||
// nacl secretbox.
|
||||
func Parse(p []byte) (Message, error) {
|
||||
@@ -96,12 +107,28 @@ func appendMsgHeader(b []byte, t MessageType, ver uint8, dataLen int) (all, data
|
||||
}
|
||||
|
||||
type Ping struct {
|
||||
// TxID is a random client-generated per-ping transaction ID.
|
||||
TxID [12]byte
|
||||
|
||||
// NodeKey is allegedly the ping sender's wireguard public key.
|
||||
// Old clients (~1.16.0 and earlier) don't send this field.
|
||||
// It shouldn't be trusted by itself, but can be combined with
|
||||
// netmap data to reduce the discokey:nodekey relation from 1:N to
|
||||
// 1:1.
|
||||
NodeKey tailcfg.NodeKey
|
||||
}
|
||||
|
||||
func (m *Ping) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePing, v0, 12)
|
||||
copy(d, m.TxID[:])
|
||||
dataLen := 12
|
||||
hasKey := !m.NodeKey.IsZero()
|
||||
if hasKey {
|
||||
dataLen += len(m.NodeKey)
|
||||
}
|
||||
ret, d := appendMsgHeader(b, TypePing, v0, dataLen)
|
||||
n := copy(d, m.TxID[:])
|
||||
if hasKey {
|
||||
copy(d[n:], m.NodeKey[:])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -110,7 +137,10 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
return nil, errShort
|
||||
}
|
||||
m = new(Ping)
|
||||
copy(m.TxID[:], p)
|
||||
p = p[copy(m.TxID[:], p):]
|
||||
if len(p) >= len(m.NodeKey) {
|
||||
copy(m.NodeKey[:], p)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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 gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package disco
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestMarshalAndParse(t *testing.T) {
|
||||
@@ -26,6 +27,19 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c",
|
||||
},
|
||||
{
|
||||
name: "ping_with_nodekey_src",
|
||||
m: &Ping{
|
||||
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
NodeKey: tailcfg.NodeKey{
|
||||
1: 1,
|
||||
2: 2,
|
||||
30: 30,
|
||||
31: 31,
|
||||
},
|
||||
},
|
||||
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1e 1f",
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
m: &Pong{
|
||||
|
||||
16
docs/bird/sample_bird.conf
Normal file
16
docs/bird/sample_bird.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
log syslog all;
|
||||
|
||||
protocol device {
|
||||
scan time 10;
|
||||
}
|
||||
|
||||
protocol bgp {
|
||||
local as 64001;
|
||||
neighbor 10.40.2.101 as 64002;
|
||||
ipv4 {
|
||||
import none;
|
||||
export all;
|
||||
};
|
||||
}
|
||||
|
||||
include "tailscale_bird.conf";
|
||||
4
docs/bird/tailscale_bird.conf
Normal file
4
docs/bird/tailscale_bird.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
protocol static tailscale {
|
||||
ipv4;
|
||||
route 100.64.0.0/10 via "tailscale0";
|
||||
}
|
||||
7
docs/k8s/Dockerfile
Executable file
7
docs/k8s/Dockerfile
Executable file
@@ -0,0 +1,7 @@
|
||||
# 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 ghcr.io/tailscale/tailscale:latest
|
||||
COPY run.sh /run.sh
|
||||
CMD "/run.sh"
|
||||
38
docs/k8s/Makefile
Normal file
38
docs/k8s/Makefile
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
|
||||
ifndef IMAGE_TAG
|
||||
$(error "IMAGE_TAG is not set")
|
||||
endif
|
||||
|
||||
ROUTES ?= ""
|
||||
SA_NAME ?= tailscale
|
||||
KUBE_SECRET ?= tailscale
|
||||
|
||||
build:
|
||||
@docker build . -t $(IMAGE_TAG)
|
||||
|
||||
push: build
|
||||
@docker push $(IMAGE_TAG)
|
||||
|
||||
rbac:
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
|
||||
|
||||
sidecar:
|
||||
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
|
||||
|
||||
userspace-sidecar:
|
||||
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
|
||||
|
||||
proxy:
|
||||
@kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{DEST_IP}};$(DEST_IP);g" | kubectl create -f-
|
||||
|
||||
subnet-router:
|
||||
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{ROUTES}};$(ROUTES);g" | kubectl create -f-
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user