Compare commits
931 Commits
rate-limit
...
c22wen/api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f1591d68 | ||
|
|
309d113f93 | ||
|
|
8fc11d582d | ||
|
|
14af677332 | ||
|
|
86fe22a1b1 | ||
|
|
56a7652dc9 | ||
|
|
13b554fed9 | ||
|
|
c2edb2865b | ||
|
|
70f14af21e | ||
|
|
0d94fe5f69 | ||
|
|
1e88050403 | ||
|
|
cf2ac2d123 | ||
|
|
2fe770ed72 | ||
|
|
ff2b3d02e6 | ||
|
|
a9a80ab372 | ||
|
|
1a42cef3a2 | ||
|
|
bfbd6b9241 | ||
|
|
80c94168ae | ||
|
|
cb96b14bf4 | ||
|
|
d0baece5fa | ||
|
|
ef15096a7d | ||
|
|
2b2a16d9a2 | ||
|
|
b4f70d8232 | ||
|
|
15c064f76f | ||
|
|
f9659323df | ||
|
|
053a1d1340 | ||
|
|
57dd247376 | ||
|
|
d97ee12179 | ||
|
|
83f45ae2dd | ||
|
|
c348fb554f | ||
|
|
90c8519765 | ||
|
|
ca676ea645 | ||
|
|
03a039d48d | ||
|
|
f5e33ad761 | ||
|
|
89be4037bb | ||
|
|
baa7937998 | ||
|
|
294ceb513c | ||
|
|
891110e64c | ||
|
|
aa353b8d0f | ||
|
|
f0b0a62873 | ||
|
|
c8c493f3d9 | ||
|
|
0ad109f63d | ||
|
|
f873da5b16 | ||
|
|
58fcd103c4 | ||
|
|
65ae66260f | ||
|
|
c9b9afd761 | ||
|
|
5f07da4854 | ||
|
|
741c513e51 | ||
|
|
554a20becb | ||
|
|
da1bad51cd | ||
|
|
fa412c8760 | ||
|
|
afcf134812 | ||
|
|
0681c6da49 | ||
|
|
2485faf69a | ||
|
|
7ea809897d | ||
|
|
9cee0bfa8c | ||
|
|
34a0292433 | ||
|
|
ce4d68b416 | ||
|
|
a6cad71fb2 | ||
|
|
a0a8b9d76a | ||
|
|
b895bf853a | ||
|
|
8a57f920ae | ||
|
|
6db9c4a173 | ||
|
|
0dc295a640 | ||
|
|
d854fe95d2 | ||
|
|
4749a96a5b | ||
|
|
338fd44657 | ||
|
|
274d32d0aa | ||
|
|
943860fde7 | ||
|
|
bce865b61b | ||
|
|
57cd7738c2 | ||
|
|
9cb6ee3777 | ||
|
|
08f94b3b50 | ||
|
|
442d1873ec | ||
|
|
19c2c6403d | ||
|
|
b3c7b631c2 | ||
|
|
05e5233e07 | ||
|
|
9503be083d | ||
|
|
88179121e3 | ||
|
|
7b92f8e718 | ||
|
|
713cbe84c1 | ||
|
|
be6fe393c5 | ||
|
|
dfbde3d3aa | ||
|
|
4c8ccd6dd6 | ||
|
|
c0af7deb86 | ||
|
|
ab482118ad | ||
|
|
c431382720 | ||
|
|
3a7402aa2d | ||
|
|
cd6099113f | ||
|
|
72e082aaf5 | ||
|
|
2c48b4ee14 | ||
|
|
0710fca0cd | ||
|
|
aa9d7f4665 | ||
|
|
a5dd0bcb09 | ||
|
|
b65eee0745 | ||
|
|
1ebbaaaebb | ||
|
|
eccc167733 | ||
|
|
8f76548fd9 | ||
|
|
5b338bf011 | ||
|
|
acade77c86 | ||
|
|
5d96ecd5e6 | ||
|
|
c8939ab7c7 | ||
|
|
883a11f2a8 | ||
|
|
d9e2edb5ae | ||
|
|
3c508a58cc | ||
|
|
51c8fd1dfc | ||
|
|
ff50ddf1ee | ||
|
|
fc8bc76e58 | ||
|
|
7a01cd27ca | ||
|
|
45d96788b5 | ||
|
|
000347d4cf | ||
|
|
b0526e8284 | ||
|
|
efad55cf86 | ||
|
|
cccdd81441 | ||
|
|
2eb474dd8d | ||
|
|
ce45f4f3ff | ||
|
|
3fdae12f0c | ||
|
|
47380ebcfb | ||
|
|
5062131aad | ||
|
|
2d604b3791 | ||
|
|
04ff3c91ee | ||
|
|
fac2b30eff | ||
|
|
a664aac877 | ||
|
|
a2d78b4d3e | ||
|
|
97e82c6cc0 | ||
|
|
19b0cfe89e | ||
|
|
258b680bc5 | ||
|
|
563d43b2a5 | ||
|
|
b246810377 | ||
|
|
c03543dbe2 | ||
|
|
0050070493 | ||
|
|
f99f6608ff | ||
|
|
a38e28da07 | ||
|
|
c2cc3acbaf | ||
|
|
d7ee3096dd | ||
|
|
9ef39af2f2 | ||
|
|
22bf48f37c | ||
|
|
55b1221db2 | ||
|
|
89894c6930 | ||
|
|
d192bd0f86 | ||
|
|
d21956436a | ||
|
|
450cfedeba | ||
|
|
e7ac9a4b90 | ||
|
|
6e52633c53 | ||
|
|
093431f5dd | ||
|
|
c48253e63b | ||
|
|
7a54910990 | ||
|
|
76d99cf01a | ||
|
|
b950bd60bf | ||
|
|
a8589636a8 | ||
|
|
b3634f020d | ||
|
|
7988f75b87 | ||
|
|
427bf2134f | ||
|
|
19df6a2ee2 | ||
|
|
ebd96bf4a9 | ||
|
|
e9bca0c00b | ||
|
|
b1de2020d7 | ||
|
|
b4e19b95ed | ||
|
|
8f30fa67aa | ||
|
|
3aa68cd397 | ||
|
|
119101962c | ||
|
|
bda53897b5 | ||
|
|
782e07c0ae | ||
|
|
4f4e84236a | ||
|
|
6bcb466096 | ||
|
|
696e160cfc | ||
|
|
946c1edb42 | ||
|
|
fb9f80cd61 | ||
|
|
ed17f5ddae | ||
|
|
39bbb86b09 | ||
|
|
28f6552646 | ||
|
|
1036f51a56 | ||
|
|
07b6ffd55c | ||
|
|
de5da37a22 | ||
|
|
65bad9a8bd | ||
|
|
20a357b386 | ||
|
|
437142daa5 | ||
|
|
710b105f38 | ||
|
|
f3aa08de76 | ||
|
|
cc3259f8d9 | ||
|
|
01ee638cca | ||
|
|
037daad47a | ||
|
|
3b46655dbb | ||
|
|
e98f2c57d6 | ||
|
|
eab6e9ea4e | ||
|
|
68ddf134d7 | ||
|
|
7e1a146e6c | ||
|
|
2b819ab38c | ||
|
|
8b904b1493 | ||
|
|
ff7ddd9d20 | ||
|
|
420838f90e | ||
|
|
508f5c3ae0 | ||
|
|
38bde61b3d | ||
|
|
c64718e9a0 | ||
|
|
09721fede8 | ||
|
|
54e6c3a290 | ||
|
|
a1ccaa9658 | ||
|
|
4a92fc9dc5 | ||
|
|
7ac91c15bd | ||
|
|
fd2a30cd32 | ||
|
|
cd07437ade | ||
|
|
d6ad41dcea | ||
|
|
e72f480d22 | ||
|
|
a3f17b8108 | ||
|
|
999bc93a4d | ||
|
|
66d196326f | ||
|
|
5b1d03f016 | ||
|
|
f33da73a82 | ||
|
|
311899709b | ||
|
|
3d34128171 | ||
|
|
4f55ebf2d9 | ||
|
|
c44e244276 | ||
|
|
9957c45995 | ||
|
|
3909c82f3d | ||
|
|
6b1d2a5630 | ||
|
|
691f1d5c1d | ||
|
|
62d941dc26 | ||
|
|
ac866054c7 | ||
|
|
22024a38c3 | ||
|
|
7c8ca28c74 | ||
|
|
6cc6e251a9 | ||
|
|
86c271caba | ||
|
|
ff0cf6340a | ||
|
|
5c35c35e7f | ||
|
|
c6dbd24f67 | ||
|
|
7a2a3955d3 | ||
|
|
a6c34bdc28 | ||
|
|
0e3048d8e0 | ||
|
|
82f2fdc194 | ||
|
|
1fd9958e9d | ||
|
|
1819f6f8c8 | ||
|
|
105a820622 | ||
|
|
551e1e99e9 | ||
|
|
746f03669c | ||
|
|
2076a50862 | ||
|
|
371f1a9502 | ||
|
|
f2ce64f0c6 | ||
|
|
515866d7c6 | ||
|
|
d027cd81df | ||
|
|
638127530b | ||
|
|
400e89367c | ||
|
|
22c462bd91 | ||
|
|
63d65368db | ||
|
|
6332bc5e08 | ||
|
|
0e5f2b90a5 | ||
|
|
5041800ac6 | ||
|
|
3e4c46259d | ||
|
|
6ee219a25d | ||
|
|
7616acd118 | ||
|
|
15297a3a09 | ||
|
|
587bdc4280 | ||
|
|
a5103a4cae | ||
|
|
585a0d8997 | ||
|
|
ed5d5f920f | ||
|
|
9784cae23b | ||
|
|
12e28aa87d | ||
|
|
cab3eb995f | ||
|
|
38dda1ea9e | ||
|
|
8051ecff55 | ||
|
|
b5a3850d29 | ||
|
|
e1596d655a | ||
|
|
ce6aca13f0 | ||
|
|
070dfa0c3d | ||
|
|
efb08e4fee | ||
|
|
c8f257df00 | ||
|
|
90b7293b3b | ||
|
|
1fecf87363 | ||
|
|
2b8d2babfa | ||
|
|
e5894aba42 | ||
|
|
4d4ca2e496 | ||
|
|
c493e5804f | ||
|
|
d3701417fc | ||
|
|
c86761cfd1 | ||
|
|
8b94a769be | ||
|
|
94a68a113b | ||
|
|
01098f41d0 | ||
|
|
73cc2d8f89 | ||
|
|
5f807c389e | ||
|
|
bbb56f2303 | ||
|
|
fddbcb0c7b | ||
|
|
0d80904fc2 | ||
|
|
f0ef561049 | ||
|
|
6e8328cba5 | ||
|
|
1fd10061fd | ||
|
|
2d0ed99672 | ||
|
|
7c11f71ac5 | ||
|
|
b7e0ff598a | ||
|
|
a601a760ba | ||
|
|
8893c2ee78 | ||
|
|
fda9dc8815 | ||
|
|
5d8b88be88 | ||
|
|
ec95e901e6 | ||
|
|
3528d28ed1 | ||
|
|
56a787fff8 | ||
|
|
fb03c60c9e | ||
|
|
963b927d5b | ||
|
|
fd77268770 | ||
|
|
5bcac4eaac | ||
|
|
4cc0ed67f9 | ||
|
|
64a24e796b | ||
|
|
afb2be71de | ||
|
|
abe095f036 | ||
|
|
3bdcfa7193 | ||
|
|
f0e9dcdc0a | ||
|
|
904a91038a | ||
|
|
c41947903a | ||
|
|
815bf017fc | ||
|
|
dea3ef0597 | ||
|
|
3aeb2e204c | ||
|
|
acafe9811f | ||
|
|
48fbe93e72 | ||
|
|
96fd20e3c0 | ||
|
|
7f97cf654d | ||
|
|
3fa863e6d9 | ||
|
|
e862f90e34 | ||
|
|
761fe19e5f | ||
|
|
88107b1287 | ||
|
|
931bcd44cb | ||
|
|
7e9d1f7808 | ||
|
|
8f5b52e571 | ||
|
|
3f4d93feb2 | ||
|
|
a5d701095b | ||
|
|
0c0239242c | ||
|
|
6e38d29485 | ||
|
|
41f6c78c53 | ||
|
|
662c19551a | ||
|
|
4f7751e025 | ||
|
|
4f71319f7c | ||
|
|
3af64765fd | ||
|
|
a084c44afc | ||
|
|
31c13013ae | ||
|
|
9ab2b32569 | ||
|
|
5a94317628 | ||
|
|
37b40b035b | ||
|
|
bc1751a376 | ||
|
|
b14288f96c | ||
|
|
23f01174ea | ||
|
|
40e12c17ec | ||
|
|
f65eb4e5c1 | ||
|
|
8b60936913 | ||
|
|
edb47b98a8 | ||
|
|
a877dd575c | ||
|
|
bf24d54143 | ||
|
|
158202dbb1 | ||
|
|
7795fcf464 | ||
|
|
22ed3c503e | ||
|
|
2e40c4b564 | ||
|
|
913c1bd04f | ||
|
|
688f923db1 | ||
|
|
96160973ce | ||
|
|
7bd89359c9 | ||
|
|
99d223130c | ||
|
|
2352690bde | ||
|
|
8ecee476f6 | ||
|
|
7fddc33481 | ||
|
|
68c42530e9 | ||
|
|
95cddfcc75 | ||
|
|
3baa084548 | ||
|
|
468bb3afce | ||
|
|
9c25968b63 | ||
|
|
82a3721661 | ||
|
|
b026a638c7 | ||
|
|
a570c27577 | ||
|
|
3b05cbacfb | ||
|
|
57e642648f | ||
|
|
6d14678009 | ||
|
|
09d56f54a7 | ||
|
|
74ee374667 | ||
|
|
1e0be5a458 | ||
|
|
3af2d671e6 | ||
|
|
9b07517f18 | ||
|
|
bd37e40d2b | ||
|
|
cb5f3c0819 | ||
|
|
5acbb149a2 | ||
|
|
2bac125cad | ||
|
|
aa1da24f18 | ||
|
|
7541982635 | ||
|
|
34a7e7c12b | ||
|
|
bc34788e65 | ||
|
|
28f9cd06f5 | ||
|
|
756d6a72bd | ||
|
|
483141094c | ||
|
|
f27a57911b | ||
|
|
f915ab6552 | ||
|
|
dd2c61a519 | ||
|
|
a67b174da1 | ||
|
|
a3fb422a39 | ||
|
|
cd7bc02ab1 | ||
|
|
5e0375808b | ||
|
|
24d1a38e81 | ||
|
|
1be6c6dd70 | ||
|
|
169ff22a84 | ||
|
|
a903d6c2ed | ||
|
|
10cad39abd | ||
|
|
9be1917c5b | ||
|
|
44598e3e89 | ||
|
|
9e2e8c80af | ||
|
|
7841c97af5 | ||
|
|
557c23517b | ||
|
|
6c71e5b851 | ||
|
|
1886dfdca3 | ||
|
|
309c15dfdd | ||
|
|
e415991256 | ||
|
|
9337a99dff | ||
|
|
4d56d19b46 | ||
|
|
9cb2df4ddd | ||
|
|
1e562886f5 | ||
|
|
461db356b9 | ||
|
|
805850add9 | ||
|
|
1af70e2468 | ||
|
|
a583e498b0 | ||
|
|
287522730d | ||
|
|
862d223c39 | ||
|
|
c5eb57f4d6 | ||
|
|
1835bb6f85 | ||
|
|
93ffc565e5 | ||
|
|
6b80bcf112 | ||
|
|
f6dc47efe4 | ||
|
|
771e9541c7 | ||
|
|
337c86b89d | ||
|
|
e64ab89712 | ||
|
|
adf4f3cce0 | ||
|
|
80d0b88a89 | ||
|
|
f90f35c123 | ||
|
|
3e2bfe48c3 | ||
|
|
062bd67d3b | ||
|
|
dbb4c246fa | ||
|
|
85c3d17b3c | ||
|
|
0512fd89a1 | ||
|
|
37c19970b3 | ||
|
|
909c165382 | ||
|
|
b983e5340f | ||
|
|
6fa7a9a055 | ||
|
|
95a18f815c | ||
|
|
b97aac1718 | ||
|
|
75225368a4 | ||
|
|
15949ad77d | ||
|
|
13661e195a | ||
|
|
1b5b59231b | ||
|
|
c2b63ba363 | ||
|
|
5a0c37aafd | ||
|
|
1f7a7a4ffe | ||
|
|
4e63a4fda3 | ||
|
|
a9b1e3f9e8 | ||
|
|
e577303dc7 | ||
|
|
355c6296f0 | ||
|
|
25b021388b | ||
|
|
84dc891843 | ||
|
|
d65e2632ab | ||
|
|
87cbc067c2 | ||
|
|
a275b9d7aa | ||
|
|
dd97111d06 | ||
|
|
696020227c | ||
|
|
b23f2263c1 | ||
|
|
c64a43a734 | ||
|
|
9318b4758c | ||
|
|
6818bb843d | ||
|
|
24f78eff62 | ||
|
|
5590daa97d | ||
|
|
b840e7dd5b | ||
|
|
1b27eb431a | ||
|
|
2622e8e082 | ||
|
|
b62b07bc2d | ||
|
|
cb01058a53 | ||
|
|
9a346fd8b4 | ||
|
|
78c2e1ff83 | ||
|
|
41c4560592 | ||
|
|
cff737786e | ||
|
|
43bc86588e | ||
|
|
2bd9ad4b40 | ||
|
|
5db529a655 | ||
|
|
934c63115e | ||
|
|
7c38db0c97 | ||
|
|
a16a793605 | ||
|
|
08949d4ef1 | ||
|
|
4987a7d46c | ||
|
|
bfcb0aa0be | ||
|
|
c3467fbadb | ||
|
|
6298018704 | ||
|
|
da3b50ad88 | ||
|
|
9e26ffecf8 | ||
|
|
d64de1ddf7 | ||
|
|
358cd3fd92 | ||
|
|
28e52a0492 | ||
|
|
43b271cb26 | ||
|
|
3e493e0417 | ||
|
|
c253d4f948 | ||
|
|
8c850947db | ||
|
|
cb970539a6 | ||
|
|
92e9a5ac15 | ||
|
|
915f65ddae | ||
|
|
c180abd7cf | ||
|
|
7cc8fcb784 | ||
|
|
b4d97d2532 | ||
|
|
ff8c8db9d3 | ||
|
|
2072dcc127 | ||
|
|
6013462e9e | ||
|
|
60c00605d3 | ||
|
|
f81233524f | ||
|
|
2ce2b63239 | ||
|
|
154d1cde05 | ||
|
|
cbf71d5eba | ||
|
|
b3fc61b132 | ||
|
|
9ff5b380cb | ||
|
|
4aba86cc03 | ||
|
|
d55fdd4669 | ||
|
|
d96d26c22a | ||
|
|
c7582dc234 | ||
|
|
3e3c24b8f6 | ||
|
|
91d95dafd2 | ||
|
|
77cad13c70 | ||
|
|
84f2320972 | ||
|
|
f8e4c75f6b | ||
|
|
33a748bec1 | ||
|
|
b77d752623 | ||
|
|
cd21ba0a71 | ||
|
|
58b721f374 | ||
|
|
ec4feaf31c | ||
|
|
41d0c81859 | ||
|
|
9beea8b314 | ||
|
|
b62341d308 | ||
|
|
9265296b33 | ||
|
|
0249236cc0 | ||
|
|
c3958898f1 | ||
|
|
7578c815be | ||
|
|
c3994fd77c | ||
|
|
5455c64f1d | ||
|
|
f794493b4f | ||
|
|
f582eeabd1 | ||
|
|
a2b4ad839b | ||
|
|
25288567ec | ||
|
|
5a370d545a | ||
|
|
37903a9056 | ||
|
|
bca9fe35ba | ||
|
|
38b0c3eea2 | ||
|
|
43e2efe441 | ||
|
|
fe68841dc7 | ||
|
|
69f3ceeb7c | ||
|
|
990e2f1ae9 | ||
|
|
961b9c8abf | ||
|
|
e298327ba8 | ||
|
|
be3ca5cbfd | ||
|
|
4970e771ab | ||
|
|
3669296cef | ||
|
|
0a42b0a726 | ||
|
|
16a9cfe2f4 | ||
|
|
5066b824a6 | ||
|
|
648268192b | ||
|
|
a89d610a3d | ||
|
|
318751c486 | ||
|
|
4957360ecd | ||
|
|
dd4e06f383 | ||
|
|
c53ab3111d | ||
|
|
05a79d79ae | ||
|
|
48fc9026e9 | ||
|
|
3b0514ef6d | ||
|
|
32ecdea157 | ||
|
|
2545575dd5 | ||
|
|
189d86cce5 | ||
|
|
218de6d530 | ||
|
|
de11f90d9d | ||
|
|
972a42cb33 | ||
|
|
d60917c0f1 | ||
|
|
f26b409bd5 | ||
|
|
6095a9b423 | ||
|
|
f745e1c058 | ||
|
|
ca2428ecaf | ||
|
|
d8e67ca2ab | ||
|
|
f562c35c0d | ||
|
|
f267a7396f | ||
|
|
c06d2a8513 | ||
|
|
bf195cd3d8 | ||
|
|
7cf50f6c84 | ||
|
|
3efc29d39d | ||
|
|
a3e7252ce6 | ||
|
|
5df6be9d38 | ||
|
|
52969bdfb0 | ||
|
|
a6559a8924 | ||
|
|
75e1cc1dd5 | ||
|
|
10ac066013 | ||
|
|
d74c9aa95b | ||
|
|
c976264bd1 | ||
|
|
f3e2b65637 | ||
|
|
380ee76d00 | ||
|
|
891898525c | ||
|
|
1f923124bf | ||
|
|
852136a03c | ||
|
|
65d2537c05 | ||
|
|
8163521c33 | ||
|
|
a2267aae99 | ||
|
|
cdfea347d0 | ||
|
|
44baa3463f | ||
|
|
45578b47f3 | ||
|
|
723b9eecb0 | ||
|
|
df674d4189 | ||
|
|
d361511512 | ||
|
|
19d77ce6a3 | ||
|
|
7ba148e54e | ||
|
|
19867b2b6d | ||
|
|
60f4982f9b | ||
|
|
bcbd41102c | ||
|
|
c3736250a4 | ||
|
|
d9ac2ada45 | ||
|
|
3b36400e35 | ||
|
|
c9e40abfb8 | ||
|
|
23123907c0 | ||
|
|
2f15894a10 | ||
|
|
fa45d606fa | ||
|
|
30bbbe9467 | ||
|
|
6e8f0860af | ||
|
|
969206fe88 | ||
|
|
e589c76e98 | ||
|
|
39ecb37fd6 | ||
|
|
c1d9e41bef | ||
|
|
f98706bdb3 | ||
|
|
61abab999e | ||
|
|
6255ce55df | ||
|
|
88e8456e9b | ||
|
|
1f7b1a4c6c | ||
|
|
b3d65ba943 | ||
|
|
5eedbcedd1 | ||
|
|
0ed9f62ed0 | ||
|
|
977381f9cc | ||
|
|
6c74065053 | ||
|
|
edcbb5394e | ||
|
|
21d1dbfce0 | ||
|
|
7815633821 | ||
|
|
98ffd78251 | ||
|
|
dba9b96908 | ||
|
|
96994ec431 | ||
|
|
0551bec95b | ||
|
|
96d806789f | ||
|
|
248d28671b | ||
|
|
bd59bba8e6 | ||
|
|
a8b95571fb | ||
|
|
de875a4d87 | ||
|
|
ecf5d69c7c | ||
|
|
3984f9be2f | ||
|
|
5280d039c4 | ||
|
|
0d481030f3 | ||
|
|
67ebba90e1 | ||
|
|
ce1b52bb71 | ||
|
|
4b75a27969 | ||
|
|
c1cabe75dc | ||
|
|
724ad13fe1 | ||
|
|
4db60a8436 | ||
|
|
742b8b44a8 | ||
|
|
5c6d8e3053 | ||
|
|
6196b7e658 | ||
|
|
32156330a8 | ||
|
|
c3c607e78a | ||
|
|
cf74e9039e | ||
|
|
0a5ab533c1 | ||
|
|
b9a95e6ce1 | ||
|
|
0fc15dcbd5 | ||
|
|
5132edacf7 | ||
|
|
9fbe8d7cf2 | ||
|
|
c9089c82e8 | ||
|
|
3f74859bb0 | ||
|
|
630379a1d0 | ||
|
|
0ea51872c9 | ||
|
|
9a8700b02a | ||
|
|
9f930ef2bf | ||
|
|
f5f3885b5b | ||
|
|
e9643ae724 | ||
|
|
16b2bbbbbb | ||
|
|
7883e5c5e7 | ||
|
|
6c70cf7222 | ||
|
|
0aea087766 | ||
|
|
73db7e99ab | ||
|
|
d94593e884 | ||
|
|
d7bc4ec029 | ||
|
|
80a14c49c6 | ||
|
|
c53b154171 | ||
|
|
622c0d0cb3 | ||
|
|
1d4f9852a7 | ||
|
|
771eb05bcb | ||
|
|
f2e5da916a | ||
|
|
9cd4e65191 | ||
|
|
97910ce712 | ||
|
|
14b4213c17 | ||
|
|
3f4f1cfe66 | ||
|
|
a477e70632 | ||
|
|
bb1a9e4700 | ||
|
|
23c93da942 | ||
|
|
c52905abaa | ||
|
|
847b6f039b | ||
|
|
57e8931160 | ||
|
|
0f0ed3dca0 | ||
|
|
056fbee4ef | ||
|
|
6233fd7ac3 | ||
|
|
e03cc2ef57 | ||
|
|
275a20f817 | ||
|
|
77e89c4a72 | ||
|
|
710ee88e94 | ||
|
|
77d3ef36f4 | ||
|
|
9b8ca219a1 | ||
|
|
7b3c0bb7f6 | ||
|
|
47b4a19786 | ||
|
|
f7124c7f06 | ||
|
|
92252b0988 | ||
|
|
2d6e84e19e | ||
|
|
9070aacdee | ||
|
|
e96f22e560 | ||
|
|
790ef2bc5f | ||
|
|
eb4eb34f37 | ||
|
|
7ca911a5c6 | ||
|
|
a83ca9e734 | ||
|
|
a975e86bb8 | ||
|
|
72bfea2ece | ||
|
|
6f73f2c15a | ||
|
|
103c06cc68 | ||
|
|
9258d64261 | ||
|
|
23e74a0f7a | ||
|
|
fe50cd0c48 | ||
|
|
b8edb7a5e9 | ||
|
|
0071888a17 | ||
|
|
4732722b87 | ||
|
|
dd43d9bc5f | ||
|
|
3553512a71 | ||
|
|
36e9cb948f | ||
|
|
894e3bfc96 | ||
|
|
19d95e095a | ||
|
|
5bc29e7388 | ||
|
|
2a8e064705 | ||
|
|
a8635784bc | ||
|
|
b87396b5d9 | ||
|
|
c2682553ff | ||
|
|
6fbd1abcd3 | ||
|
|
de5f6d70a8 | ||
|
|
666d404066 | ||
|
|
00ca17edf4 | ||
|
|
53fb25fc2f | ||
|
|
88c305c8af | ||
|
|
d9054da86a | ||
|
|
0ecaf7b5ed | ||
|
|
401e2ec307 | ||
|
|
58c9591a49 | ||
|
|
10368ef4c0 | ||
|
|
c12d87c54b | ||
|
|
c8cf3169ba | ||
|
|
7cbf6ab771 | ||
|
|
5d4415399b | ||
|
|
6757c990a8 | ||
|
|
08a6eeb55a | ||
|
|
d9fd5db1e1 | ||
|
|
abd79ea368 | ||
|
|
15a23ce65f | ||
|
|
a036c8c718 | ||
|
|
0371848097 | ||
|
|
4c23b5e4ea | ||
|
|
03aa319762 | ||
|
|
9dd3544e84 | ||
|
|
1f4ccae591 | ||
|
|
a447caebf8 | ||
|
|
50b2e5ffe6 | ||
|
|
8edcab04d5 | ||
|
|
51f421946f | ||
|
|
deb113838e | ||
|
|
280e8884dd | ||
|
|
d05b0500ac | ||
|
|
d1a30be275 | ||
|
|
51d176ecff | ||
|
|
07e02ec9d3 | ||
|
|
511840b1f6 | ||
|
|
5e1ee4be53 | ||
|
|
c3f7733f53 | ||
|
|
5c9ddf5e76 | ||
|
|
2ca2389c5f | ||
|
|
07ca0c1c29 | ||
|
|
39f2fe29f7 | ||
|
|
1cb7dab881 | ||
|
|
e441d3218e | ||
|
|
02231e968e | ||
|
|
6f590f5b52 | ||
|
|
1d2e497d47 | ||
|
|
059b1d10bb | ||
|
|
5e0ff494a5 | ||
|
|
4d599d194f | ||
|
|
b33c86b542 | ||
|
|
b663ab4685 | ||
|
|
5798826990 | ||
|
|
e01a4c50ba | ||
|
|
5a32f8e181 | ||
|
|
484b7fc9a3 | ||
|
|
c62b80e00b | ||
|
|
cc687fc3e6 | ||
|
|
08a38f21c9 | ||
|
|
c71754eba2 | ||
|
|
d4127db0fe | ||
|
|
0dac03876a | ||
|
|
364a8508b2 | ||
|
|
73c40c77b0 | ||
|
|
83b6b06cc4 | ||
|
|
3c7791f6bf | ||
|
|
5aae6b734d | ||
|
|
984a699219 | ||
|
|
24009241bf | ||
|
|
cf0d19f0ab | ||
|
|
722673f307 | ||
|
|
a5d6c9d616 | ||
|
|
9e5d79e2f1 | ||
|
|
becce82246 | ||
|
|
7a410f9236 | ||
|
|
45b139d338 | ||
|
|
dcd7a118d3 | ||
|
|
1e837b8e81 | ||
|
|
e7ae6a2e06 | ||
|
|
8575b21ca8 | ||
|
|
e46238a2af | ||
|
|
f0b6ba78e8 | ||
|
|
096d7a50ff | ||
|
|
765695eaa2 | ||
|
|
7f68e097dd | ||
|
|
1407540b52 | ||
|
|
5114df415e | ||
|
|
3fa58303d0 | ||
|
|
db2a216561 | ||
|
|
d3134ad0c8 | ||
|
|
7247e896b5 | ||
|
|
dd6b96ba68 | ||
|
|
cf5d25e15b | ||
|
|
004780b312 | ||
|
|
03682cb271 | ||
|
|
1617a232e1 | ||
|
|
a6bd3a7e53 | ||
|
|
e9f7d01b91 | ||
|
|
9e3ad4f79f | ||
|
|
a428656280 | ||
|
|
fff062b461 | ||
|
|
f0204098d8 | ||
|
|
0245bbe97b | ||
|
|
c5495288a6 | ||
|
|
9bbcdba2b3 | ||
|
|
a96165679c | ||
|
|
f69003fd46 | ||
|
|
9ff51909a3 | ||
|
|
a496cdc943 | ||
|
|
8a6bd21baf | ||
|
|
34c30eaea0 | ||
|
|
85d93fc4e3 | ||
|
|
99aa33469e | ||
|
|
30e5c19214 | ||
|
|
7cd9ff3dde | ||
|
|
5eb09c8f5e | ||
|
|
afb9c6a6ab | ||
|
|
2b74236567 | ||
|
|
557b310e67 | ||
|
|
737124ef70 | ||
|
|
7317e73bf4 | ||
|
|
7508b67c54 | ||
|
|
703d789005 | ||
|
|
b0c10fa610 | ||
|
|
43ded2b581 | ||
|
|
3f4a567032 | ||
|
|
e6b84f2159 | ||
|
|
e8b3a5e7a1 | ||
|
|
35a8586f7e | ||
|
|
3ed2124356 | ||
|
|
ea8f92b312 | ||
|
|
af9328c1b7 | ||
|
|
f2db4ac277 | ||
|
|
db051fb013 | ||
|
|
d074ec6571 | ||
|
|
c5fcc38bf1 | ||
|
|
d03de31404 | ||
|
|
1013cda799 | ||
|
|
806de4ac94 | ||
|
|
c97c45f268 | ||
|
|
39d20e8a75 | ||
|
|
cd2f6679bb | ||
|
|
7fb33123d3 | ||
|
|
bb55694c95 | ||
|
|
635f7b99f1 | ||
|
|
9c914dc7dd | ||
|
|
3e27b3c33c | ||
|
|
0fe262f093 | ||
|
|
c67c8913c3 | ||
|
|
292606a975 | ||
|
|
cff53c6e6d | ||
|
|
5ec7ac1d02 | ||
|
|
e6d0c92b1d | ||
|
|
d0754760e7 | ||
|
|
8f8607b6bf | ||
|
|
d53e8fc0da | ||
|
|
3b1ce30967 | ||
|
|
286f96e412 | ||
|
|
040a0d5121 | ||
|
|
fc88e34f42 | ||
|
|
4f128745d8 | ||
|
|
42a0e0c601 | ||
|
|
08acb502e5 | ||
|
|
e0b666c5d2 | ||
|
|
89a6f27cf8 | ||
|
|
a7edf11a40 | ||
|
|
fe97bedf67 | ||
|
|
33b2f30cea | ||
|
|
9ccbcda612 | ||
|
|
72cae5504c | ||
|
|
e42ec4efba | ||
|
|
3663797815 | ||
|
|
cd01bcc395 | ||
|
|
64f6104e63 | ||
|
|
bfdc8175b1 | ||
|
|
8eda667aa1 | ||
|
|
874be6566d | ||
|
|
8a3e77fc43 | ||
|
|
8b0be7475b | ||
|
|
ad1cfe8bbe | ||
|
|
21ac65d3da | ||
|
|
e00b814a24 | ||
|
|
381b94d4d1 | ||
|
|
e83d02ffd1 | ||
|
|
efc1feedc9 | ||
|
|
529e2cb31a | ||
|
|
fde384b359 | ||
|
|
e16f7e48a3 | ||
|
|
48b1e85e8a | ||
|
|
ccbd0937d0 | ||
|
|
74d6ab995d | ||
|
|
0c69b4e00d | ||
|
|
499c8fcbb3 | ||
|
|
b01db109f5 | ||
|
|
b8f01eed34 | ||
|
|
8861bb5a19 | ||
|
|
6802481bf5 | ||
|
|
78b1ed39ea | ||
|
|
c9de43cd59 | ||
|
|
89af51b84d | ||
|
|
89198b1691 | ||
|
|
7618d7e677 |
4
.github/workflows/cross-darwin.yml
vendored
4
.github/workflows/cross-darwin.yml
vendored
@@ -3,7 +3,7 @@ name: Darwin-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
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
@@ -3,7 +3,7 @@ name: FreeBSD-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
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
@@ -3,7 +3,7 @@ name: OpenBSD-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
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
@@ -3,7 +3,7 @@ name: Windows-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
28
.github/workflows/depaware.yml
vendored
Normal file
28
.github/workflows/depaware.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: depaware
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: depaware tailscaled
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
|
||||
- name: depaware tailscale
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
4
.github/workflows/license.yml
vendored
4
.github/workflows/license.yml
vendored
@@ -3,7 +3,7 @@ name: license
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -3,7 +3,7 @@ name: Linux
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
48
.github/workflows/linux32.yml
vendored
Normal file
48
.github/workflows/linux32.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Linux 32-bit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Basic build
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
7
.github/workflows/staticcheck.yml
vendored
7
.github/workflows/staticcheck.yml
vendored
@@ -3,7 +3,7 @@ name: staticcheck
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,11 +16,14 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Print staticcheck version
|
||||
run: go run honnef.co/go/tools/cmd/staticcheck -version
|
||||
|
||||
|
||||
52
.github/workflows/windows.yml
vendored
Normal file
52
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,12 +1,11 @@
|
||||
# Binaries for programs and plugins
|
||||
*~
|
||||
*.tmp
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
cmd/relaynode/relaynode
|
||||
cmd/taillogin/taillogin
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
@@ -18,3 +17,7 @@ cmd/tailscaled/tailscaled
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# direnv config, this may be different for other people so it's probably safer
|
||||
# to make this nonspecific.
|
||||
.envrc
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.14-alpine AS build-env
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -34,5 +34,5 @@ COPY . .
|
||||
RUN go install -v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache ca-certificates iptables
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
18
Makefile
Normal file
18
Makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
updatedeps:
|
||||
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
|
||||
|
||||
depaware:
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
check: staticcheck vet depaware
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
37
README.md
37
README.md
@@ -6,25 +6,44 @@ Private WireGuard® networks made easy
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains all the open source Tailscale code.
|
||||
It currently includes the Linux client.
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
|
||||
The Linux client is currently `cmd/relaynode`, but will
|
||||
soon be replaced by `cmd/tailscaled`.
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
https://pkgs.tailscale.com .
|
||||
|
||||
## Other clients
|
||||
|
||||
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
|
||||
use the code in this repository but additionally include small GUI
|
||||
wrappers that are not open source.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
|
||||
If you're packaging Tailscale for distribution, use `build_dist.sh`
|
||||
instead, to burn commit IDs and version info into the binaries:
|
||||
|
||||
```
|
||||
./build_dist.sh tailscale.com/cmd/tailscale
|
||||
./build_dist.sh tailscale.com/cmd/tailscaled
|
||||
```
|
||||
|
||||
If your distro has conventions that preclude the use of
|
||||
`build_dist.sh`, please do the equivalent of what it does in your
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.14) in module mode. It might
|
||||
release candidate builds (currently Go 1.15) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
@@ -35,10 +54,8 @@ Please file any issues about this code or the hosted service on
|
||||
|
||||
## Contributing
|
||||
|
||||
`under_construction.gif`
|
||||
|
||||
PRs welcome, but we are still working out our contribution process and
|
||||
tooling.
|
||||
PRs welcome! But please file bugs. Commit messages should [reference
|
||||
bugs](https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls).
|
||||
|
||||
We require [Developer Certificate of
|
||||
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
@@ -46,7 +63,7 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
|
||||
## About Us
|
||||
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney,
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
|
||||
from Tailscale Inc.
|
||||
You can learn more about us from [our website](https://tailscale.com).
|
||||
|
||||
|
||||
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.3.0
|
||||
661
api.md
Normal file
661
api.md
Normal file
@@ -0,0 +1,661 @@
|
||||
# Tailscale API
|
||||
|
||||
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.
|
||||
|
||||
# APIS
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
##### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/device/12345
|
||||
curl https://api.tailscale.com/api/v2/device/12345?fields=all \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
"100.105.58.116"
|
||||
],
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"name":"user1-device.example.com",
|
||||
"hostname":"User1-Device",
|
||||
"clientVersion":"date.20201107",
|
||||
"updateAvailable":false,
|
||||
"os":"macOS",
|
||||
"created":"2020-11-20T20:56:49Z",
|
||||
"lastSeen":"2020-11-20T16:15:55-05:00",
|
||||
"keyExpiryDisabled":false,
|
||||
"expires":"2021-05-19T20:56:49Z",
|
||||
"authorized":true,
|
||||
"isExternal":false,
|
||||
"machineKey":"mkey:user1-machine-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"blocksIncomingConnections":false,
|
||||
"enabledRoutes":[
|
||||
|
||||
],
|
||||
"advertisedRoutes":[
|
||||
|
||||
],
|
||||
"clientConnectivity": {
|
||||
"endpoints":[
|
||||
"209.195.87.231:59128",
|
||||
"192.168.0.173:59128"
|
||||
],
|
||||
"derp":"",
|
||||
"mappingVariesByDestIP":false,
|
||||
"latency":{
|
||||
"Dallas":{
|
||||
"latencyMs":60.463043
|
||||
},
|
||||
"New York City":{
|
||||
"preferred":true,
|
||||
"latencyMs":31.323811
|
||||
},
|
||||
"San Francisco":{
|
||||
"latencyMs":81.313389
|
||||
}
|
||||
},
|
||||
"clientSupports":{
|
||||
"hairPinning":false,
|
||||
"ipv6":false,
|
||||
"pcp":false,
|
||||
"pmp":false,
|
||||
"udp":true,
|
||||
"upnp":false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl https://api.tailscale.com/api/v2/device/11055/routes \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : []
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`routes` - The new list of enabled subnet routes in JSON.
|
||||
```
|
||||
{
|
||||
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl https://api.tailscale.com/api/v2/device/11055/routes \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]}'
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Domain
|
||||
<!---
|
||||
TODO: ctrl+f domain, replace with {workgroup/tailnet/other}
|
||||
Domain is a top level resource. ACL is an example of a resource that is tied to a top level domain.
|
||||
--->
|
||||
|
||||
### ACL
|
||||
|
||||
#### `GET /api/v2/domain/:domain/acl` - fetch ACL for a domain
|
||||
|
||||
Retrieves the ACL that is currently set for the given domain. Supply the domain of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
##### Returns
|
||||
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
|
||||
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
|
||||
|
||||
##### Example
|
||||
|
||||
###### Requesting a HuJSON response:
|
||||
```
|
||||
GET /api/v2/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/hujson" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
"Tests": [],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": [
|
||||
"*"
|
||||
],
|
||||
"Ports": [
|
||||
"*:*"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
```
|
||||
GET /api/v2/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/json" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
{
|
||||
"acls" : [
|
||||
{
|
||||
"action" : "accept",
|
||||
"ports" : [
|
||||
"*:*"
|
||||
],
|
||||
"users" : [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups" : {
|
||||
"group:example" : [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
]
|
||||
},
|
||||
"hosts" : {
|
||||
"example-host-1" : "100.100.100.100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/acl` - set ACL for a domain
|
||||
|
||||
Sets the ACL for the given domain. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
|
||||
|
||||
Returns error for invalid ACLs.
|
||||
Returns error if using an `If-Match` header and the ETag does not match.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "If-Match: \"e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c\""
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/acl/preview` - preview rule matches on an ACL for a resource
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`user` - A user's email. The provided ACL is queried with this user to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/acl/preiew
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/acl?user=user1@example.com \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
### Devices
|
||||
#### `GET /api/v2/domain/:domain/devices` - list the devices for a domain
|
||||
Lists the devices for a domain.
|
||||
Supply the domain of interest in the path.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: Returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/domain/example.com/devices
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/devices \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
{
|
||||
"addresses":[
|
||||
"100.68.203.125"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user1-device.example.com",
|
||||
"created":"2020-11-30T22:20:04Z",
|
||||
"lastSeen":"2020-11-30T17:20:04-05:00",
|
||||
"hostname":"User1-Device",
|
||||
"machineKey":"mkey:user1-node-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"expires":"2021-05-29T22:20:04Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
},
|
||||
{
|
||||
"addresses":[
|
||||
"100.111.63.90"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user2-device.example.com",
|
||||
"created":"2020-11-30T22:21:03Z",
|
||||
"lastSeen":"2020-11-30T17:21:03-05:00",
|
||||
"hostname":"User2-Device",
|
||||
"machineKey":"mkey:user2-machine-key",
|
||||
"nodeKey":"nodekey:user2-node-key",
|
||||
"id":"48810",
|
||||
"user":"user2@example.com",
|
||||
"expires":"2021-05-29T22:21:03Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### DNS
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/nameservers` - list the DNS nameservers for a domain
|
||||
Lists the DNS nameservers for a domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/nameservers
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/nameservers` - replaces the list of DNS nameservers for a domain
|
||||
Replaces the list of DNS nameservers for the given domain with the list supplied by the user.
|
||||
Supply the domain of interest in the path.
|
||||
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
`dns` - The new list of DNS nameservers in JSON.
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
Returns the new list of nameservers and the status of MagicDNS.
|
||||
|
||||
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
|
||||
|
||||
##### Example
|
||||
###### Adding DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": ["8.8.8.8"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
###### Removing all DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": []}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
"magicDNS": false,
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/preferences` - retrieves the DNS preferences for a domain
|
||||
Retrieves the DNS preferences that are currently set for the given domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/preferences` - replaces the DNS preferences for a domain
|
||||
Replaces the DNS preferences for a domain, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
|
||||
If there is at least one DNS server, then MagicDNS can be enabled.
|
||||
Otherwise, it returns an error.
|
||||
Note that removing all nameservers will turn off MagicDNS.
|
||||
To reenable it, nameservers must be added back, and MagicDNS must be explicity turned on.
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your network.
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"magicDNS": true}'
|
||||
```
|
||||
|
||||
|
||||
Response:
|
||||
|
||||
If there are no DNS servers, it returns an error message:
|
||||
```
|
||||
{
|
||||
"message":"need at least one nameserver to enable MagicDNS"
|
||||
}
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/searchpaths` - retrieves the search paths for a domain
|
||||
Retrieves the list of search paths that is currently set for the given domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/searchpaths` - replaces the search paths for a domain
|
||||
Replaces the list of search paths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`searchPaths` - A list of searchpaths in JSON format.
|
||||
```
|
||||
{
|
||||
"searchPaths: ["user1.example.com", "user2.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
}
|
||||
```
|
||||
@@ -9,20 +9,39 @@
|
||||
package atomicfile // import "tailscale.com/atomicfile"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
tmpname := filename + ".new.tmp"
|
||||
if err := ioutil.WriteFile(tmpname, data, perm); err != nil {
|
||||
return fmt.Errorf("%#v: %v", tmpname, err)
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpname, filename); err != nil {
|
||||
return fmt.Errorf("%#v->%#v: %v", tmpname, filename, err)
|
||||
tmpName := f.Name()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := f.Chmod(perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpName, filename)
|
||||
}
|
||||
|
||||
16
build_dist.sh
Executable file
16
build_dist.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
#
|
||||
# Runs `go build` with flags configured for binary distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
# If you're packaging Tailscale for a distro, please consider using
|
||||
# this script, or executing equivalent commands in your
|
||||
# distro-specific build system.
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
exec go build -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}" "$@"
|
||||
309
cmd/cloner/cloner.go
Normal file
309
cmd/cloner/cloner.go
Normal file
@@ -0,0 +1,309 @@
|
||||
// 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.
|
||||
|
||||
// Cloner is a tool to automate the creation of a Clone method.
|
||||
//
|
||||
// The result of the Clone method aliases no memory that can be edited
|
||||
// with the original.
|
||||
//
|
||||
// This tool makes lots of implicit assumptions about the types you feed it.
|
||||
// In particular, it can only write relatively "shallow" Clone methods.
|
||||
// That is, if a type contains another named struct type, cloner assumes that
|
||||
// named type will also have a Clone method.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var (
|
||||
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
||||
flagOutput = flag.String("output", "", "output file; required")
|
||||
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("cloner: ")
|
||||
flag.Parse()
|
||||
if len(*flagTypes) == 0 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
typeNames := strings.Split(*flagTypes, ",")
|
||||
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedName,
|
||||
Tests: false,
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
cfg.BuildFlags = []string{"-tags=" + *flagBuildTags}
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, ".")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
log.Fatalf("wrong number of packages: %d", len(pkgs))
|
||||
}
|
||||
pkg := pkgs[0]
|
||||
buf := new(bytes.Buffer)
|
||||
imports := make(map[string]struct{})
|
||||
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 {
|
||||
log.Fatalf("could not find type %s", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
w := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(buf, format+"\n", args...)
|
||||
}
|
||||
if *flagCloneFunc {
|
||||
w("// Clone duplicates src into dst and reports whether it succeeded.")
|
||||
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
|
||||
w("// where T is one of %s.", *flagTypes)
|
||||
w("func Clone(dst, src interface{}) bool {")
|
||||
w(" switch src := src.(type) {")
|
||||
for _, typeName := range typeNames {
|
||||
w(" case *%s:", typeName)
|
||||
w(" switch dst := dst.(type) {")
|
||||
w(" case *%s:", typeName)
|
||||
w(" *dst = *src.Clone()")
|
||||
w(" return true")
|
||||
w(" case **%s:", typeName)
|
||||
w(" *dst = src.Clone()")
|
||||
w(" return true")
|
||||
w(" }")
|
||||
}
|
||||
w(" }")
|
||||
w(" return false")
|
||||
w("}")
|
||||
}
|
||||
|
||||
contents := new(bytes.Buffer)
|
||||
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
|
||||
fmt.Fprintf(contents, "import (\n")
|
||||
for s := range imports {
|
||||
fmt.Fprintf(contents, "\t%q\n", s)
|
||||
}
|
||||
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, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type %s; DO NOT EDIT.
|
||||
|
||||
package %s
|
||||
|
||||
`
|
||||
|
||||
func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types.Named, thisPkg *types.Package) {
|
||||
pkgQual := func(pkg *types.Package) string {
|
||||
if thisPkg == pkg {
|
||||
return ""
|
||||
}
|
||||
imports[pkg.Path()] = struct{}{}
|
||||
return pkg.Name()
|
||||
}
|
||||
importedName := func(t types.Type) string {
|
||||
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("\t\t" + `panic("TODO map value pointers")`)
|
||||
} 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())
|
||||
}
|
||||
}
|
||||
|
||||
func hasBasicUnderlying(typ types.Type) bool {
|
||||
switch typ.Underlying().(type) {
|
||||
case *types.Slice, *types.Map:
|
||||
return true
|
||||
default:
|
||||
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
|
||||
}
|
||||
@@ -7,11 +7,13 @@ package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -20,18 +22,20 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/stun"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -42,10 +46,12 @@ var (
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
PrivateKey wgkey.Private
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -57,7 +63,7 @@ func loadConfig() config {
|
||||
}
|
||||
b, err := ioutil.ReadFile(*configPath)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return writeNewConfig()
|
||||
case err != nil:
|
||||
log.Fatal(err)
|
||||
@@ -71,8 +77,8 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgcfg.PrivateKey {
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -118,6 +124,22 @@ func main() {
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
b, err := ioutil.ReadFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
key := strings.TrimSpace(string(b))
|
||||
if matched, _ := regexp.MatchString(`(?i)^[0-9a-f]{64,}$`, key); !matched {
|
||||
log.Fatalf("key in %s must contain 64+ hex digits", *meshPSKFile)
|
||||
}
|
||||
s.SetMeshKey(key)
|
||||
log.Printf("DERP mesh key configured")
|
||||
}
|
||||
if err := startMesh(s); err != nil {
|
||||
log.Fatalf("startMesh: %v", err)
|
||||
}
|
||||
expvar.Publish("derp", s.ExpVar())
|
||||
|
||||
// Create our own mux so we don't expose /debug/ stuff to the world.
|
||||
@@ -165,8 +187,17 @@ func main() {
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := letsEncryptGetCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{mux}))
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
@@ -185,19 +216,31 @@ func main() {
|
||||
|
||||
func debugHandler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.RequestURI == "/debug/check" {
|
||||
err := s.ConsistencyCheck()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
} else {
|
||||
io.WriteString(w, "derp.Server ConsistencyCheck okay")
|
||||
}
|
||||
return
|
||||
}
|
||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
||||
f(`<html><body>
|
||||
<h1>DERP debug</h1>
|
||||
<ul>
|
||||
`)
|
||||
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
|
||||
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
|
||||
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
|
||||
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
|
||||
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
|
||||
|
||||
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
|
||||
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
|
||||
<li><a href="/debug/pprof/">/debug/pprof/</a></li>
|
||||
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine</a> (collapsed)</li>
|
||||
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine</a> (full)</li>
|
||||
<li><a href="/debug/check">/debug/check</a> internal consistency check</li>
|
||||
<ul>
|
||||
</html>
|
||||
`)
|
||||
@@ -268,7 +311,7 @@ func serveSTUN() {
|
||||
}
|
||||
}
|
||||
|
||||
var validProdHostname = regexp.MustCompile(`^derp(\d+|\-\w+)?\.tailscale\.com\.?$`)
|
||||
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
|
||||
|
||||
func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
if validProdHostname.MatchString(host) {
|
||||
@@ -276,3 +319,16 @@ func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
}
|
||||
return errors.New("invalid hostname")
|
||||
}
|
||||
|
||||
func defaultMeshPSKFile() string {
|
||||
try := []string{
|
||||
"/home/derp/keys/derp-mesh.key",
|
||||
filepath.Join(os.Getenv("HOME"), "keys", "derp-mesh.key"),
|
||||
}
|
||||
for _, p := range try {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
{"derp.tailscale.com", true},
|
||||
{"derp.tailscale.com.", true},
|
||||
{"derp1.tailscale.com", true},
|
||||
{"derp1b.tailscale.com", true},
|
||||
{"derp2.tailscale.com", true},
|
||||
{"derp02.tailscale.com", true},
|
||||
{"derp-nyc.tailscale.com", true},
|
||||
{"derpfoo.tailscale.com", false},
|
||||
{"derpfoo.tailscale.com", true},
|
||||
{"derp02.bar.tailscale.com", false},
|
||||
{"example.net", false},
|
||||
}
|
||||
|
||||
45
cmd/derper/mesh.go
Normal file
45
cmd/derper/mesh.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func startMesh(s *derp.Server) error {
|
||||
if *meshWith == "" {
|
||||
return nil
|
||||
}
|
||||
if !s.HasMeshKey() {
|
||||
return errors.New("--mesh-with requires --mesh-psk-file")
|
||||
}
|
||||
for _, host := range strings.Split(*meshWith, ",") {
|
||||
if err := startMeshWithHost(s, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startMeshWithHost(s *derp.Server, host string) error {
|
||||
logf := logger.WithPrefix(log.Printf, fmt.Sprintf("mesh(%q): ", host))
|
||||
c, err := derphttp.NewClient(s.PrivateKey(), "https://"+host+"/derp", logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.MeshKey = s.MeshKey()
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,7 @@ var (
|
||||
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() {
|
||||
@@ -65,12 +67,15 @@ func main() {
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: ch.GetCertificate,
|
||||
},
|
||||
}
|
||||
|
||||
if err := httpsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +93,16 @@ func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# placeholder to work around redo bug
|
||||
145
cmd/tailscale/cli/cli.go
Normal file
145
cmd/tailscale/cli/cli.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package cli contains the cmd/tailscale CLI code in a package that can be included
|
||||
// in other wrapper binaries such as the Mac and Windows clients.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
// ActLikeCLI reports whether a GUI application should act like the
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
func ActLikeCLI() bool {
|
||||
if len(os.Args) < 2 {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) error {
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
}
|
||||
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
return c, bc, ctx, cancel
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
175
cmd/tailscale/cli/debug.go
Normal file
175
cmd/tailscale/cli/debug.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
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)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
66
cmd/tailscale/cli/down.go
Normal file
66
cmd/tailscale/cli/down.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, func() {
|
||||
log.Fatalf("timeout running stop")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
cur := n.Status.BackendState
|
||||
switch cur {
|
||||
case "Stopped":
|
||||
log.Printf("already stopped")
|
||||
cancel()
|
||||
default:
|
||||
log.Printf("was in state %q", cur)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.State != nil {
|
||||
log.Printf("now in state %q", *n.State)
|
||||
if *n.State == ipn.Stopped {
|
||||
cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
bc.SetWantRunning(false)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
174
cmd/tailscale/cli/netcheck.go
Normal file
174
cmd/tailscale/cli/netcheck.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var netcheckCmd = &ffcli.Command{
|
||||
Name: "netcheck",
|
||||
ShortUsage: "netcheck",
|
||||
ShortHelp: "Print an analysis of local network conditions",
|
||||
Exec: runNetcheck,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
|
||||
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")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var netcheckArgs struct {
|
||||
format string
|
||||
every time.Duration
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
c.Verbose = true
|
||||
} else {
|
||||
c.Logf = logger.Discard
|
||||
}
|
||||
|
||||
if strings.HasPrefix(netcheckArgs.format, "json") {
|
||||
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
|
||||
}
|
||||
|
||||
dm := derpmap.Prod()
|
||||
for {
|
||||
t0 := time.Now()
|
||||
report, err := c.GetReport(ctx, dm)
|
||||
d := time.Since(t0)
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("netcheck: %v", err)
|
||||
}
|
||||
if err := printReport(dm, report); err != nil {
|
||||
return err
|
||||
}
|
||||
if netcheckArgs.every == 0 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(netcheckArgs.every)
|
||||
}
|
||||
}
|
||||
|
||||
func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
var j []byte
|
||||
var err error
|
||||
switch netcheckArgs.format {
|
||||
case "":
|
||||
break
|
||||
case "json":
|
||||
j, err = json.MarshalIndent(report, "", "\t")
|
||||
case "json-line":
|
||||
j, err = json.Marshal(report)
|
||||
default:
|
||||
return fmt.Errorf("unknown output format %q", netcheckArgs.format)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j != nil {
|
||||
j = append(j, '\n')
|
||||
os.Stdout.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nReport:\n")
|
||||
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
||||
if report.GlobalV4 != "" {
|
||||
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
} else {
|
||||
fmt.Printf("\t* IPv4: (no addr found)\n")
|
||||
}
|
||||
if report.GlobalV6 != "" {
|
||||
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
fmt.Printf("\t* IPv6: (no addr found)\n")
|
||||
} else {
|
||||
fmt.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))
|
||||
|
||||
// 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")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
}
|
||||
sort.Slice(rids, func(i, j int) bool {
|
||||
l1, ok1 := report.RegionLatency[rids[i]]
|
||||
l2, ok2 := report.RegionLatency[rids[j]]
|
||||
if ok1 != ok2 {
|
||||
return ok1 // defined things sort first
|
||||
}
|
||||
if !ok1 {
|
||||
return rids[i] < rids[j]
|
||||
}
|
||||
return l1 < l2
|
||||
})
|
||||
for _, rid := range rids {
|
||||
d, ok := report.RegionLatency[rid]
|
||||
var latency string
|
||||
if ok {
|
||||
latency = d.Round(time.Millisecond / 10).String()
|
||||
}
|
||||
r := dm.Regions[rid]
|
||||
var derpNum string
|
||||
if netcheckArgs.verbose {
|
||||
derpNum = fmt.Sprintf("derp%d, ", rid)
|
||||
}
|
||||
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func portMapping(r *netcheck.Report) string {
|
||||
if !r.AnyPortMappingChecked() {
|
||||
return "not checked"
|
||||
}
|
||||
var got []string
|
||||
if r.UPnP.EqualBool(true) {
|
||||
got = append(got, "UPnP")
|
||||
}
|
||||
if r.PMP.EqualBool(true) {
|
||||
got = append(got, "NAT-PMP")
|
||||
}
|
||||
if r.PCP.EqualBool(true) {
|
||||
got = append(got, "PCP")
|
||||
}
|
||||
return strings.Join(got, ", ")
|
||||
}
|
||||
130
cmd/tailscale/cli/ping.go
Normal file
130
cmd/tailscale/cli/ping.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var pingCmd = &ffcli.Command{
|
||||
Name: "ping",
|
||||
ShortUsage: "ping <hostname-or-IP>",
|
||||
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale ping' command pings a peer node at the Tailscale layer
|
||||
and reports which route it took for each response. The first ping or
|
||||
so will likely go over DERP (Tailscale's TCP relay protocol) while NAT
|
||||
traversal finds a direct path through.
|
||||
|
||||
If 'tailscale ping' works but a normal ping does not, that means one
|
||||
side's operating system firewall is blocking packets; 'tailscale ping'
|
||||
does not inject packets into either side's TUN devices.
|
||||
|
||||
By default, 'tailscale ping' stops after 10 pings or once a direct
|
||||
(non-DERP) path has been established, whichever comes first.
|
||||
|
||||
The provided hostname must resolve to or be a Tailscale IP
|
||||
(e.g. 100.x.y.z) or a subnet IP advertised by a Tailscale
|
||||
relay node.
|
||||
|
||||
`),
|
||||
Exec: runPing,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pingArgs struct {
|
||||
num int
|
||||
untilDirect bool
|
||||
verbose bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
n++
|
||||
bc.Ping(ip)
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
}
|
||||
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
|
||||
via := pr.Endpoint
|
||||
if pr.DERPRegionID != 0 {
|
||||
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
|
||||
}
|
||||
anyPong = true
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
if pr.Endpoint != "" && pingArgs.untilDirect {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if n == pingArgs.num {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
@@ -24,13 +25,15 @@ import (
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [-web] [-json]",
|
||||
ShortUsage: "status [-active] [-web] [-json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||
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)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
@@ -42,12 +45,16 @@ var statusArgs struct {
|
||||
web bool // run webserver
|
||||
listen string // in web mode, webserver address to listen on, empty means auto
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
ch := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
@@ -73,6 +80,13 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
if statusArgs.json {
|
||||
if statusArgs.active {
|
||||
for peer, ps := range st.Peer {
|
||||
if !peerActive(ps) {
|
||||
delete(st.Peer, peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
j, err := json.MarshalIndent(st, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -113,18 +127,30 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if st.BackendState == ipn.Stopped.String() {
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
peer.ShortString(),
|
||||
ps.PublicKey.ShortString(),
|
||||
ps.OS,
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
ps.TxBytes,
|
||||
ps.RxBytes,
|
||||
)
|
||||
relay := ps.Relay
|
||||
if active && relay != "" && ps.CurAddr == "" {
|
||||
relay = "*" + relay + "*"
|
||||
} else {
|
||||
relay = " " + relay
|
||||
}
|
||||
f("%-6s", relay)
|
||||
for i, addr := range ps.Addrs {
|
||||
if i != 0 {
|
||||
f(", ")
|
||||
@@ -137,6 +163,28 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.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() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
270
cmd/tailscale/cli/up.go
Normal file
270
cmd/tailscale/cli/up.go
Normal file
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
Name: "up",
|
||||
ShortUsage: "up [flags]",
|
||||
ShortHelp: "Connect to your Tailscale network",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
"tailscale up" connects this machine to your Tailscale network,
|
||||
triggering authentication if necessary.
|
||||
|
||||
The flags passed to this command are specific to this machine. If you don't
|
||||
specify any flags, options are reset to their default.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
}
|
||||
return upf
|
||||
})(),
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
return "off"
|
||||
}
|
||||
return "on"
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings if IP forwarding is not
|
||||
// enabled, or if we were unable to verify the state of IP forwarding.
|
||||
func checkIPForwarding() {
|
||||
var key string
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
key = "net.ipv4.ip_forward"
|
||||
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
warnf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
warnf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
if !on {
|
||||
warnf("%s is disabled. Subnet routes won't work.", key)
|
||||
}
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
return errors.New("--advertise-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
var printed bool
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(os.Stderr, "Success.\n")
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
// StateKey and send the prefs directly. Although the Windows
|
||||
// supports server mode, though, the transition to StateStore
|
||||
// is only half complete. Only server mode uses it, and the
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection idenity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
// on our connection's identity.
|
||||
opts.StateKey = ""
|
||||
opts.Prefs = prefs
|
||||
}
|
||||
|
||||
// We still have to Start right now because it's the only way to
|
||||
// set up notifications and whatnot. This causes a bunch of churn
|
||||
// every time the CLI touches anything.
|
||||
//
|
||||
// TODO(danderson): redo the frontend/backend API to assume
|
||||
// ephemeral frontends that read/modify/write state, once
|
||||
// Windows/Mac state is moved into backend.
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
}
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
70
cmd/tailscale/cli/version.go
Normal file
70
cmd/tailscale/cli/version.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var versionCmd = &ffcli.Command{
|
||||
Name: "version",
|
||||
ShortUsage: "version [flags]",
|
||||
ShortHelp: "Print Tailscale version",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("version", flag.ExitOnError)
|
||||
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
|
||||
return fs
|
||||
})(),
|
||||
Exec: runVersion,
|
||||
}
|
||||
|
||||
var versionArgs struct {
|
||||
daemon bool // also check local node's daemon version
|
||||
}
|
||||
|
||||
func runVersion(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
if !versionArgs.daemon {
|
||||
fmt.Println(version.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
fmt.Printf("Daemon: %s\n", n.Version)
|
||||
close(done)
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
212
cmd/tailscale/depaware.txt
Normal file
212
cmd/tailscale/depaware.txt
Normal file
@@ -0,0 +1,212 @@
|
||||
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http+
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from 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
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
L os/user from github.com/godbus/dbus/v5
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf8 from bufio+
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/netcheck"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var netcheckCmd = &ffcli.Command{
|
||||
Name: "netcheck",
|
||||
ShortUsage: "netcheck",
|
||||
ShortHelp: "Print an analysis of local network conditions",
|
||||
Exec: runNetcheck,
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
DERP: derpmap.Prod(),
|
||||
Logf: logger.WithPrefix(log.Printf, "netcheck: "),
|
||||
DNSCache: dnscache.Get(),
|
||||
}
|
||||
|
||||
report, err := c.GetReport(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("netcheck: %v", err)
|
||||
}
|
||||
fmt.Printf("\nReport:\n")
|
||||
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
||||
if report.GlobalV4 != "" {
|
||||
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
} else {
|
||||
fmt.Printf("\t* IPv4: (no addr found)\n")
|
||||
}
|
||||
if report.GlobalV6 != "" {
|
||||
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
fmt.Printf("\t* IPv6: (no addr found)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* IPv6: no\n")
|
||||
}
|
||||
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
|
||||
// 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.DERPLatency) == 0 {
|
||||
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v (%v)\n", report.PreferredDERP, c.DERP.LocationOfID(report.PreferredDERP))
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
var ss []string
|
||||
for s := range report.DERPLatency {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
for _, s := range ss {
|
||||
fmt.Printf("\t\t- %s = %v\n", s, report.DERPLatency[s])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -7,222 +7,22 @@
|
||||
package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
// startup.
|
||||
//
|
||||
// We have to support multiple state keys for other OSes (Windows in
|
||||
// particular), but right now Unix daemons run with a single
|
||||
// node-global state. To keep open the option of having per-user state
|
||||
// later, the global state key doesn't look like a username.
|
||||
const globalStateKey = "_daemon"
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.noSingleRoutes, "no-single-routes", false, "don't install routes to single nodes")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upCmd := &ffcli.Command{
|
||||
Name: "up",
|
||||
ShortUsage: "up [flags]",
|
||||
ShortHelp: "Connect to your Tailscale network",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
"tailscale up" connects this machine to your Tailscale network,
|
||||
triggering authentication if necessary.
|
||||
|
||||
The flags passed to this command are specific to this machine. If you don't
|
||||
specify any flags, options are reset to their default.
|
||||
`),
|
||||
FlagSet: upf,
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && err != flag.ErrHelp {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
noSingleRoutes bool
|
||||
shieldsUp bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
authKey string
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
var routes []wgcfg.CIDR
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
cidr, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
log.Fatalf("%q is not a valid CIDR prefix: %v", s, err)
|
||||
}
|
||||
routes = append(routes, cidr)
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
log.Fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
// TODO(apenwarr): allow setting/using CorpDNS
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.AllowSingleHosts = !upArgs.noSingleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
opts := ipn.Options{
|
||||
StateKey: globalStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
bc.StartLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
fmt.Fprintf(os.Stderr, "tailscaled is authenticated, nothing more to do.\n")
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
},
|
||||
}
|
||||
// We still have to Start right now because it's the only way to
|
||||
// set up notifications and whatnot. This causes a bunch of churn
|
||||
// every time the CLI touches anything.
|
||||
//
|
||||
// TODO(danderson): redo the frontend/backend API to assume
|
||||
// ephemeral frontends that read/modify/write state, once
|
||||
// Windows/Mac state is moved into backend.
|
||||
bc.Start(opts)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
log.Fatalf("--socket cannot be empty")
|
||||
}
|
||||
log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
return c, bc, ctx, cancel
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
228
cmd/tailscaled/depaware.txt
Normal file
228
cmd/tailscaled/depaware.txt
Normal file
@@ -0,0 +1,228 @@
|
||||
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/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/snappy 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 github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from 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
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template+
|
||||
html/template from net/http/pprof
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/trace from net/http/pprof
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf8 from bufio+
|
||||
@@ -11,18 +11,29 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
@@ -34,80 +45,154 @@ import (
|
||||
// later, the global state key doesn't look like a username.
|
||||
const globalStateKey = "_daemon"
|
||||
|
||||
var defaultTunName = "tailscale0"
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "openbsd" {
|
||||
defaultTunName = "tun"
|
||||
// defaultTunName returns the default tun device name for the platform.
|
||||
func defaultTunName() string {
|
||||
switch runtime.GOOS {
|
||||
case "openbsd":
|
||||
return "tun"
|
||||
case "windows":
|
||||
return "Tailscale"
|
||||
}
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
var args struct {
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
}
|
||||
|
||||
func main() {
|
||||
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
|
||||
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
|
||||
tunname := getopt.StringLong("tun", 0, defaultTunName, "tunnel interface name")
|
||||
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
|
||||
statepath := getopt.StringLong("state", 0, paths.DefaultTailscaledStateFile(), "Path of state file")
|
||||
socketpath := getopt.StringLong("socket", 's', paths.DefaultTailscaledSocket(), "Path of the service unix socket")
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
// allocations. So let's be aggressive about garbage collection,
|
||||
// unless the user specifically overrides it in the usual way.
|
||||
if _, ok := os.LookupEnv("GOGC"); !ok {
|
||||
debug.SetGCPercent(10)
|
||||
}
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
logf("fixConsoleOutput: %v", err)
|
||||
}
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
log.Fatalf("fixConsoleOutput: %v", err)
|
||||
}
|
||||
|
||||
if *statepath == "" {
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
||||
}
|
||||
|
||||
if printVersion {
|
||||
fmt.Println(version.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
if *socketpath == "" {
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
if err := run(); err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
router.Cleanup(logf, args.tunname)
|
||||
return nil
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if *debug != "" {
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, *debug)
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
var e wgengine.Engine
|
||||
if *fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport)
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("wgengine.New: %v", err)
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
}
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// Exit gracefully by cancelling the ipnserver context in most common cases:
|
||||
// interrupted from the TTY or killed by a service manager.
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
// SIGPIPE sometimes gets generated when CLIs disconnect from
|
||||
// tailscaled. The default action is to terminate the process, we
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// continue
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: *socketpath,
|
||||
SocketPath: args.socketpath,
|
||||
Port: 41112,
|
||||
StatePath: *statepath,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
LegacyConfigPath: paths.LegacyConfigPath,
|
||||
LegacyConfigPath: paths.LegacyConfigPath(),
|
||||
SurviveDisconnects: true,
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e)
|
||||
if err != nil {
|
||||
log.Fatalf("tailscaled: %v", err)
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(crawshaw): It would be nice to start a timeout context the moment a signal
|
||||
// is received and use that timeout to give us a moment to finish uploading logs
|
||||
// here. But the signal is handled inside ipnserver.Run, so some plumbing is needed.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
pol.Shutdown(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
|
||||
@@ -3,12 +3,12 @@ Description=Tailscale node agent
|
||||
Documentation=https://tailscale.com/kb/
|
||||
Wants=network-pre.target
|
||||
After=network-pre.target
|
||||
StartLimitIntervalSec=0
|
||||
StartLimitBurst=0
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
Restart=on-failure
|
||||
|
||||
@@ -18,6 +18,18 @@ StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -23,38 +23,40 @@ import (
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// TODO(apenwarr): eliminate the 'state' variable, as it's now obsolete.
|
||||
// It's used only by the unit tests.
|
||||
type state int
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
// TODO(apenwarr): eliminate 'state', as it's now obsolete.
|
||||
type State int
|
||||
|
||||
const (
|
||||
stateNew = state(iota)
|
||||
stateNotAuthenticated
|
||||
stateAuthenticating
|
||||
stateURLVisitRequired
|
||||
stateAuthenticated
|
||||
stateSynchronized // connected and received map update
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s state) MarshalText() ([]byte, error) {
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s state) String() string {
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case stateNew:
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case stateNotAuthenticated:
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case stateAuthenticating:
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case stateURLVisitRequired:
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case stateAuthenticated:
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case stateSynchronized:
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
@@ -69,7 +71,7 @@ type Status struct {
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
state state
|
||||
State State
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
@@ -84,7 +86,7 @@ func (s *Status) Equal(s2 *Status) bool {
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.state == s2.state
|
||||
s.State == s2.State
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
@@ -92,7 +94,7 @@ func (s Status) String() string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.state.String() + " " + string(b)
|
||||
return s.State.String() + " " + string(b)
|
||||
}
|
||||
|
||||
type LoginGoal struct {
|
||||
@@ -115,13 +117,15 @@ type Client struct {
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state state
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -167,6 +171,28 @@ func NewNoStart(opts Options) (*Client, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
func (c *Client) SetPaused(paused bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if paused == c.paused {
|
||||
return
|
||||
}
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Only cancel the map routine. (The auth routine isn't expensive
|
||||
// so it's fine to keep it running.)
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
for _, ch := range c.unpauseWaiters {
|
||||
close(ch)
|
||||
}
|
||||
c.unpauseWaiters = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
@@ -205,7 +231,7 @@ func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("cancelMapSafely: synced=%v", c.synced)
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
// received at least one netmap since the last
|
||||
@@ -227,88 +253,57 @@ func (c *Client) cancelMapSafely() {
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("cancelMapSafely: wrote to channel")
|
||||
c.logf("[v1] cancelMapSafely: wrote to channel")
|
||||
default:
|
||||
// if channel write failed, then there was already
|
||||
// an outstanding newMapCh request. One is enough,
|
||||
// since it'll always use the latest endpoints.
|
||||
c.logf("cancelMapSafely: channel was full")
|
||||
c.logf("[v1] cancelMapSafely: channel was full")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.Backoff{Name: "authRoutine"}
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
synced := c.synced
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil", c.state)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("authRoutine: quit")
|
||||
c.logf("[v1] authRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v", msg, err)
|
||||
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 {
|
||||
c.sendStatus("authRoutine1", err, "", nil)
|
||||
c.sendStatus("authRoutine-report", err, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
// Wait for something interesting to happen
|
||||
var exp <-chan time.Time
|
||||
if expiry != nil && !expiry.IsZero() {
|
||||
// if expiry is in the future, don't delay
|
||||
// past that time.
|
||||
// If it's in the past, then it's already
|
||||
// being handled by someone, so no need to
|
||||
// wake ourselves up again.
|
||||
now := c.timeNow()
|
||||
if expiry.Before(now) {
|
||||
delay := expiry.Sub(now)
|
||||
if delay > 5*time.Second {
|
||||
delay = time.Second
|
||||
}
|
||||
exp = time.After(delay)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("authRoutine: context done.")
|
||||
case <-exp:
|
||||
// Unfortunately the key expiry isn't provided
|
||||
// by the control server until mapRequest.
|
||||
// So we have to do some hackery with c.expiry
|
||||
// in here.
|
||||
// TODO(apenwarr): add a key expiry field in RegisterResponse.
|
||||
c.logf("authRoutine: key expiration check.")
|
||||
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
|
||||
c.logf("Key expired; setting loggedIn=false.")
|
||||
// Wait for user to Login or Logout.
|
||||
<-ctx.Done()
|
||||
c.logf("[v1] authRoutine: context done.")
|
||||
continue
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: c.loggedIn,
|
||||
}
|
||||
c.loggedIn = false
|
||||
c.expiry = nil
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(c.authCtx)
|
||||
if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(ctx)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -319,18 +314,18 @@ func (c *Client) authRoutine() {
|
||||
c.mu.Lock()
|
||||
c.loggedIn = false
|
||||
c.loginGoal = nil
|
||||
c.state = stateNotAuthenticated
|
||||
c.state = StateNotAuthenticated
|
||||
c.synced = false
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine2", nil, "", nil)
|
||||
c.sendStatus("authRoutine-wantout", nil, "", nil)
|
||||
bo.BackOff(ctx, nil)
|
||||
} else { // ie. goal.wantLoggedIn
|
||||
c.mu.Lock()
|
||||
if goal.url != "" {
|
||||
c.state = stateURLVisitRequired
|
||||
c.state = StateURLVisitRequired
|
||||
} else {
|
||||
c.state = stateAuthenticating
|
||||
c.state = StateAuthenticating
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -353,17 +348,18 @@ func (c *Client) authRoutine() {
|
||||
err = fmt.Errorf("weird: server required a new url?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
goal.url = url
|
||||
goal.token = nil
|
||||
goal.flags = LoginDefault
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = goal
|
||||
c.state = stateURLVisitRequired
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: true,
|
||||
flags: LoginDefault,
|
||||
url: url,
|
||||
}
|
||||
c.state = StateURLVisitRequired
|
||||
c.synced = false
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine3", err, url, nil)
|
||||
c.sendStatus("authRoutine-url", err, url, nil)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
@@ -372,22 +368,59 @@ func (c *Client) authRoutine() {
|
||||
c.mu.Lock()
|
||||
c.loggedIn = true
|
||||
c.loginGoal = nil
|
||||
c.state = stateAuthenticated
|
||||
c.state = StateAuthenticated
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine4", nil, "", nil)
|
||||
c.sendStatus("authRoutine-success", nil, "", nil)
|
||||
c.cancelMapSafely()
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expiry returns the credential expiration time, or the zero time if
|
||||
// the expiration time isn't known. Used in tests only.
|
||||
func (c *Client) Expiry() *time.Time {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.expiry
|
||||
}
|
||||
|
||||
// Direct returns the underlying direct client object. Used in tests
|
||||
// only.
|
||||
func (c *Client) Direct() *Direct {
|
||||
return c.direct
|
||||
}
|
||||
|
||||
// unpausedChanLocked returns a new channel that is closed when the
|
||||
// current Client pause is unpaused.
|
||||
//
|
||||
// c.mu must be held
|
||||
func (c *Client) unpausedChanLocked() <-chan struct{} {
|
||||
unpaused := make(chan struct{})
|
||||
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
|
||||
return unpaused
|
||||
}
|
||||
|
||||
func (c *Client) mapRoutine() {
|
||||
defer close(c.mapDone)
|
||||
bo := backoff.Backoff{Name: "mapRoutine"}
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
if c.paused {
|
||||
unpaused := c.unpausedChanLocked()
|
||||
c.mu.Unlock()
|
||||
c.logf("mapRoutine: awaiting unpause")
|
||||
select {
|
||||
case <-unpaused:
|
||||
c.logf("mapRoutine: unpaused")
|
||||
case <-c.quit:
|
||||
c.logf("mapRoutine: quit")
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.logf("mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
ctx := c.mapCtx
|
||||
@@ -401,7 +434,7 @@ func (c *Client) mapRoutine() {
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v", msg, err)
|
||||
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.
|
||||
@@ -436,7 +469,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.logf("[v1] mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.cancelMapLocked()
|
||||
|
||||
// Don't emit this netmap; we're
|
||||
@@ -449,7 +482,7 @@ func (c *Client) mapRoutine() {
|
||||
c.synced = true
|
||||
c.inPollNetMap = true
|
||||
if c.loggedIn {
|
||||
c.state = stateSynchronized
|
||||
c.state = StateSynchronized
|
||||
}
|
||||
exp := nm.Expiry
|
||||
c.expiry = &exp
|
||||
@@ -458,20 +491,26 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
c.logf("[v1] mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine2", nil, "", nm)
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
})
|
||||
|
||||
c.mu.Lock()
|
||||
c.synced = false
|
||||
c.inPollNetMap = false
|
||||
if c.state == stateSynchronized {
|
||||
c.state = stateAuthenticated
|
||||
if c.state == StateSynchronized {
|
||||
c.state = StateAuthenticated
|
||||
}
|
||||
paused := c.paused
|
||||
c.mu.Unlock()
|
||||
|
||||
if paused {
|
||||
c.logf("mapRoutine: paused")
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
report(err, "PollNetMap")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -500,7 +539,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
if !c.direct.SetHostinfo(hi) {
|
||||
c.logf("[unexpected] duplicate Hostinfo: %v", hi)
|
||||
// No changes. Don't log.
|
||||
return
|
||||
}
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
@@ -533,11 +572,11 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("sendStatus: %s: %v", who, state)
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *Persist
|
||||
var fin *empty.Message
|
||||
if state == stateAuthenticated {
|
||||
if state == StateAuthenticated {
|
||||
fin = new(empty.Message)
|
||||
}
|
||||
if nm != nil && loggedIn && synced {
|
||||
@@ -554,7 +593,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Hostinfo: hi,
|
||||
state: state,
|
||||
State: state,
|
||||
}
|
||||
if err != nil {
|
||||
new.Err = err.Error()
|
||||
@@ -623,3 +662,20 @@ func (c *Client) Shutdown() {
|
||||
c.logf("Client.Shutdown done.")
|
||||
}
|
||||
}
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
func (c *Client) TestOnlySetAuthKey(authkey string) {
|
||||
c.direct.mu.Lock()
|
||||
defer c.direct.mu.Unlock()
|
||||
c.direct.authKey = authkey
|
||||
}
|
||||
|
||||
func (c *Client) TestOnlyTimeNow() time.Time {
|
||||
return c.timeNow()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
|
||||
func TestStatusEqual(t *testing.T) {
|
||||
// Verify that the Equal method stays in sync with reality
|
||||
equalHandles := []string{"LoginFinished", "Err", "URL", "Persist", "NetMap", "Hostinfo", "state"}
|
||||
equalHandles := []string{"LoginFinished", "Err", "URL", "Persist", "NetMap", "Hostinfo", "State"}
|
||||
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
|
||||
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, equalHandles)
|
||||
@@ -48,13 +48,13 @@ func TestStatusEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Status{state: stateNew},
|
||||
&Status{state: stateNew},
|
||||
&Status{State: StateNew},
|
||||
&Status{State: StateNew},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Status{state: stateNew},
|
||||
&Status{state: stateAuthenticated},
|
||||
&Status{State: StateNew},
|
||||
&Status{State: StateAuthenticated},
|
||||
false,
|
||||
},
|
||||
{
|
||||
@@ -70,3 +70,10 @@ func TestStatusEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package controlclient
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -18,27 +21,50 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type Persist struct {
|
||||
_ structs.Incomparable
|
||||
PrivateMachineKey wgcfg.PrivateKey
|
||||
PrivateNodeKey wgcfg.PrivateKey
|
||||
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
|
||||
_ structs.Incomparable
|
||||
|
||||
// LegacyFrontendPrivateMachineKey is here temporarily
|
||||
// (starting 2020-09-28) during migration of Windows users'
|
||||
// machine keys from frontend storage to the backend. On the
|
||||
// first LocalBackend.Start call, the backend will initialize
|
||||
// the real (backend-owned) machine key from the frontend's
|
||||
// provided value (if non-zero), picking a new random one if
|
||||
// needed. This field should be considered read-only from GUI
|
||||
// frontends. The real value should not be written back in
|
||||
// this field, lest the frontend persist it to disk.
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
@@ -51,7 +77,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.PrivateMachineKey.Equal(p2.PrivateMachineKey) &&
|
||||
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
|
||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||
p.Provider == p2.Provider &&
|
||||
@@ -59,9 +85,9 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgcfg.Key
|
||||
if !p.PrivateMachineKey.IsZero() {
|
||||
mk = p.PrivateMachineKey.Public()
|
||||
var mk, ok, nk wgkey.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
if !p.OldPrivateNodeKey.IsZero() {
|
||||
ok = p.OldPrivateNodeKey.Public()
|
||||
@@ -69,9 +95,14 @@ func (p *Persist) Pretty() string {
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
return fmt.Sprintf("Persist{m=%v, o=%v, n=%v u=%#v}",
|
||||
mk.ShortString(), ok.ShortString(), nk.ShortString(),
|
||||
p.LoginName)
|
||||
ss := func(k wgkey.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
ss(mk), ss(ok), ss(nk), p.LoginName)
|
||||
}
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
@@ -83,28 +114,36 @@ type Direct struct {
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgcfg.Key
|
||||
serverKey wgkey.Key
|
||||
persist Persist
|
||||
authKey string
|
||||
tryingNewKey wgcfg.PrivateKey
|
||||
tryingNewKey wgkey.Private
|
||||
expiry *time.Time
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
endpoints []string
|
||||
localPort uint16 // or zero to mean auto
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
endpoints []string
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
localPort uint16 // or zero to mean auto
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
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
|
||||
NewDecompressor func() (Decompressor, error)
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
Persist Persist // initial persistent data
|
||||
MachinePrivateKey wgkey.Private // 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
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -117,6 +156,9 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
if opts.ServerURL == "" {
|
||||
return nil, errors.New("controlclient.New: no server URL specified")
|
||||
}
|
||||
if opts.MachinePrivateKey.IsZero() {
|
||||
return nil, errors.New("controlclient.New: no MachinePrivateKey specified")
|
||||
}
|
||||
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
|
||||
serverURL, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
@@ -133,7 +175,15 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
httpc = &http.Client{Transport: tr}
|
||||
@@ -141,6 +191,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -148,6 +199,8 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -157,12 +210,20 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.LONG,
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +240,8 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
return false
|
||||
}
|
||||
c.hostinfo = hi.Clone()
|
||||
j, _ := json.Marshal(c.hostinfo)
|
||||
c.logf("HostInfo: %s", j)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -223,16 +286,14 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
|
||||
// TODO(crawshaw): Tell the server. This node key should be
|
||||
// immediately invalidated.
|
||||
//if c.persist.PrivateNodeKey != (wgcfg.PrivateKey{}) {
|
||||
//if !c.persist.PrivateNodeKey.IsZero() {
|
||||
//}
|
||||
c.persist = Persist{
|
||||
PrivateMachineKey: c.persist.PrivateMachineKey,
|
||||
}
|
||||
c.persist = Persist{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(%v, %v)", t != nil, flags)
|
||||
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, t, flags, false, "")
|
||||
}
|
||||
|
||||
@@ -249,6 +310,7 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi
|
||||
if mustregen {
|
||||
_, url, err = c.doLogin(ctx, t, flags, true, url)
|
||||
}
|
||||
|
||||
return url, err
|
||||
}
|
||||
|
||||
@@ -257,20 +319,19 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
persist := c.persist
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverKey
|
||||
authKey := c.authKey
|
||||
hostinfo := c.hostinfo.Clone()
|
||||
backendLogID := hostinfo.BackendLogID
|
||||
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
|
||||
c.mu.Unlock()
|
||||
|
||||
if persist.PrivateMachineKey == (wgcfg.PrivateKey{}) {
|
||||
c.logf("Generating a new machinekey.")
|
||||
mkey, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
persist.PrivateMachineKey = mkey
|
||||
if c.machinePrivKey.IsZero() {
|
||||
return false, "", errors.New("controlclient.Direct requires a machine private key")
|
||||
}
|
||||
|
||||
if expired {
|
||||
c.logf("Old key expired -> regen=true")
|
||||
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
||||
regen = true
|
||||
}
|
||||
if (flags & LoginInteractive) != 0 {
|
||||
@@ -279,7 +340,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
}
|
||||
|
||||
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
|
||||
if serverKey == (wgcfg.Key{}) {
|
||||
if serverKey.IsZero() {
|
||||
var err error
|
||||
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
|
||||
if err != nil {
|
||||
@@ -291,12 +352,12 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var oldNodeKey wgcfg.Key
|
||||
var oldNodeKey wgkey.Key
|
||||
if url != "" {
|
||||
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
|
||||
} else if regen || persist.PrivateNodeKey.IsZero() {
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
c.logf("login keygen: %v", err)
|
||||
return regen, url, err
|
||||
@@ -306,14 +367,14 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
// Try refreshing the current key first
|
||||
tryingNewKey = persist.PrivateNodeKey
|
||||
}
|
||||
if persist.OldPrivateNodeKey != (wgcfg.PrivateKey{}) {
|
||||
if !persist.OldPrivateNodeKey.IsZero() {
|
||||
oldNodeKey = persist.OldPrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
if tryingNewKey == (wgcfg.PrivateKey{}) {
|
||||
if tryingNewKey.IsZero() {
|
||||
log.Fatalf("tryingNewKey is empty, give up")
|
||||
}
|
||||
if c.hostinfo.BackendLogID == "" {
|
||||
if backendLogID == "" {
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, url, err
|
||||
}
|
||||
@@ -321,7 +382,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
Version: 1,
|
||||
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
|
||||
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
|
||||
Hostinfo: c.hostinfo,
|
||||
Hostinfo: hostinfo,
|
||||
Followup: url,
|
||||
}
|
||||
c.logf("RegisterReq: onode=%v node=%v fup=%v",
|
||||
@@ -330,14 +391,14 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
request.Auth.Oauth2Token = t
|
||||
request.Auth.Provider = persist.Provider
|
||||
request.Auth.LoginName = persist.LoginName
|
||||
request.Auth.AuthKey = c.authKey
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
request.Auth.AuthKey = authKey
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, c.machinePrivKey.Public().HexString())
|
||||
req, err := http.NewRequest("POST", u, body)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
@@ -348,11 +409,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
if err != nil {
|
||||
return regen, url, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
c.logf("RegisterReq: returned.")
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return regen, url, fmt.Errorf("register request: http %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
|
||||
if err := decode(res, &resp, &serverKey, &c.machinePrivKey); err != nil {
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, c.machinePrivKey.Public(), err)
|
||||
return regen, url, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
// Log without PII:
|
||||
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
|
||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||
|
||||
if resp.NodeKeyExpired {
|
||||
if regen {
|
||||
@@ -376,7 +446,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
// - user is disabled
|
||||
|
||||
if resp.AuthURL != "" {
|
||||
c.logf("AuthURL is %.20v...", resp.AuthURL)
|
||||
c.logf("AuthURL is %v", resp.AuthURL)
|
||||
} else {
|
||||
c.logf("No AuthURL")
|
||||
}
|
||||
@@ -428,6 +498,9 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
|
||||
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
|
||||
c.localPort = localPort
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
c.everEndpoints = true
|
||||
}
|
||||
return true // changed
|
||||
}
|
||||
|
||||
@@ -440,58 +513,85 @@ func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed boo
|
||||
return c.newEndpoints(localPort, endpoints)
|
||||
}
|
||||
|
||||
var debugNetmap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETMAP"))
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
// PollNetMap makes a /map request to download the network map, calling cb with
|
||||
// each new netmap.
|
||||
//
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverKey := c.serverKey
|
||||
hostinfo := c.hostinfo
|
||||
hostinfo := c.hostinfo.Clone()
|
||||
backendLogID := hostinfo.BackendLogID
|
||||
localPort := c.localPort
|
||||
ep := append([]string(nil), c.endpoints...)
|
||||
everEndpoints := c.everEndpoints
|
||||
c.mu.Unlock()
|
||||
|
||||
if hostinfo.BackendLogID == "" {
|
||||
if backendLogID == "" {
|
||||
return errors.New("hostinfo: BackendLogID missing")
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
|
||||
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if debugNetmap {
|
||||
if Debug.NetMap {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
Version: 4,
|
||||
IncludeIPv6: includeIPv6(),
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
Version: 8,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
DebugFlags: c.debugFlags,
|
||||
}
|
||||
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
|
||||
old := request.DebugFlags
|
||||
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
|
||||
}
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
}
|
||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
||||
// to tell the control server not to distribute out our (empty) endpoints to peers.
|
||||
// Presumably we'll learn our endpoints in a half second and do another post
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyperspecific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
if len(ep) == 0 && !everEndpoints && !inTest() {
|
||||
request.ReadOnly = true
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
machinePubKey := tailcfg.MachineKey(c.machinePrivKey.Public())
|
||||
t0 := time.Now()
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
res, err := c.httpc.Do(req)
|
||||
if err != nil {
|
||||
@@ -502,7 +602,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("initial fetch failed %d: %s",
|
||||
return fmt.Errorf("initial fetch failed %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
defer res.Body.Close()
|
||||
@@ -540,6 +640,10 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
}()
|
||||
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
// away, followed by a delay, and eventually others.
|
||||
@@ -547,6 +651,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
// the same format before just closing the connection.
|
||||
// We can use this same read loop either way.
|
||||
var msg []byte
|
||||
var previousPeers []*tailcfg.Node // for delta-purposes
|
||||
for i := 0; i < maxPolls || maxPolls < 0; i++ {
|
||||
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
|
||||
var siz [4]byte
|
||||
@@ -568,43 +673,102 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
vlogf("netmap: decode error: %v")
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent keep-alive timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("netmap: not resetting timer for keep-alive due to: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("[v1] netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
continue
|
||||
}
|
||||
vlogf("netmap: got new map")
|
||||
|
||||
undeltaPeers(&resp, previousPeers)
|
||||
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
for _, up := range resp.UserProfiles {
|
||||
lastUserProfile[up.ID] = up
|
||||
}
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
lastDERPMap = resp.DERPMap
|
||||
}
|
||||
if resp.Debug != nil {
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
}
|
||||
// Temporarily (2020-06-29) support removing all but
|
||||
// discovery-supporting nodes during development, for
|
||||
// less noise.
|
||||
if Debug.OnlyDisco {
|
||||
filtered := resp.Peers[:0]
|
||||
for _, p := range resp.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
}
|
||||
|
||||
nm := &NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
Roles: resp.Roles,
|
||||
DNS: resp.DNS,
|
||||
DNSDomains: resp.SearchPaths,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
for _, profile := range resp.UserProfiles {
|
||||
nm.UserProfiles[profile.ID] = profile
|
||||
addUserProfile := func(userID tailcfg.UserID) {
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
// Already populated it from a previous peer.
|
||||
return
|
||||
}
|
||||
if up, ok := lastUserProfile[userID]; ok {
|
||||
nm.UserProfiles[userID] = up
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
for _, peer := range resp.Peers {
|
||||
addUserProfile(peer.User)
|
||||
}
|
||||
if resp.Node.MachineAuthorized {
|
||||
nm.MachineStatus = tailcfg.MachineAuthorized
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
@@ -614,7 +778,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -629,7 +793,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -641,11 +805,16 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
|
||||
return decodeMsg(msg, v, serverKey, mkey)
|
||||
}
|
||||
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
mkey := c.persist.PrivateMachineKey
|
||||
serverKey := c.serverKey
|
||||
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||
|
||||
decrypted, err := decryptMsg(msg, &serverKey, &mkey)
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
decrypted, err := decryptMsg(msg, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -653,7 +822,6 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
if c.newDecompressor == nil {
|
||||
b = decrypted
|
||||
} else {
|
||||
//decoder, err := zstd.NewReader(nil)
|
||||
decoder, err := c.newDecompressor()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -664,6 +832,15 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if debugMap {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
}
|
||||
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient.Direct.decodeMsg into %T: %q", v, b)
|
||||
}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
@@ -671,18 +848,21 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
decrypted, err := decryptMsg(msg, serverKey, mkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
}
|
||||
if err := json.Unmarshal(decrypted, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
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))
|
||||
@@ -693,18 +873,17 @@ func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byt
|
||||
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")
|
||||
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 *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const debugMapRequests = false
|
||||
if debugMapRequests {
|
||||
if debugMap {
|
||||
if _, ok := v.(tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
@@ -718,38 +897,253 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgcfg.Key, error) {
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
|
||||
req, err := http.NewRequest("GET", serverURL+"/key", nil)
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgkey.Key{}, 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 wgcfg.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
}
|
||||
key, err := wgcfg.ParseHexKey(string(b))
|
||||
key, err := wgkey.ParseHex(string(b))
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// includeIPv6 reports whether we should enable IPv6 for magicsock
|
||||
// connections. This is only here temporarily (2020-03-26) as a
|
||||
// opt-out in case there are problems.
|
||||
func includeIPv6() bool {
|
||||
if e := os.Getenv("DEBUG_INCLUDE_IPV6"); e != "" {
|
||||
v, _ := strconv.ParseBool(e)
|
||||
return v
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
func envBool(k string) bool {
|
||||
e := os.Getenv(k)
|
||||
if e == "" {
|
||||
return false
|
||||
}
|
||||
v, err := strconv.ParseBool(e)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid non-bool %q for env var %q", e, k))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
|
||||
// and the PeersRemoved and PeersChanged fields in mapRes.
|
||||
// It then also nils out the delta fields.
|
||||
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if len(mapRes.Peers) > 0 {
|
||||
// Not delta encoded.
|
||||
if !nodesSorted(mapRes.Peers) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
||||
sortNodes(mapRes.Peers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var removed map[tailcfg.NodeID]bool
|
||||
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
||||
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
||||
for _, id := range pr {
|
||||
removed[id] = true
|
||||
}
|
||||
}
|
||||
changed := mapRes.PeersChanged
|
||||
|
||||
if len(removed) == 0 && len(changed) == 0 {
|
||||
// No changes fast path.
|
||||
mapRes.Peers = prev
|
||||
return
|
||||
}
|
||||
|
||||
if !nodesSorted(changed) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
||||
sortNodes(changed)
|
||||
}
|
||||
if !nodesSorted(prev) {
|
||||
// Internal error (unrelated to the network) if we get here.
|
||||
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
||||
sortNodes(prev)
|
||||
}
|
||||
|
||||
newFull := make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
||||
for len(prev) > 0 && len(changed) > 0 {
|
||||
pID := prev[0].ID
|
||||
cID := changed[0].ID
|
||||
if removed[pID] {
|
||||
prev = prev[1:]
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case pID < cID:
|
||||
newFull = append(newFull, prev[0])
|
||||
prev = prev[1:]
|
||||
case pID == cID:
|
||||
newFull = append(newFull, changed[0])
|
||||
prev, changed = prev[1:], changed[1:]
|
||||
case cID < pID:
|
||||
newFull = append(newFull, changed[0])
|
||||
changed = changed[1:]
|
||||
}
|
||||
}
|
||||
newFull = append(newFull, changed...)
|
||||
for _, n := range prev {
|
||||
if !removed[n.ID] {
|
||||
newFull = append(newFull, n)
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
}
|
||||
|
||||
func nodesSorted(v []*tailcfg.Node) bool {
|
||||
for i, n := range v {
|
||||
if i > 0 && n.ID <= v[i-1].ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sortNodes(v []*tailcfg.Node) {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
||||
}
|
||||
|
||||
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
if v1 == nil {
|
||||
return nil
|
||||
}
|
||||
v2 := make([]*tailcfg.Node, len(v1))
|
||||
for i, n := range v1 {
|
||||
v2[i] = n.Clone()
|
||||
}
|
||||
return v2
|
||||
}
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute atomic.Value
|
||||
controlTrimWGConfig atomic.Value
|
||||
)
|
||||
|
||||
func setControlAtomic(dst *atomic.Value, v opt.Bool) {
|
||||
old, ok := dst.Load().(opt.Bool)
|
||||
if !ok || old != v {
|
||||
dst.Store(v)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPRouteFlag reports the last reported value from control for whether
|
||||
// DERP route optimization (Issue 150) should be enabled.
|
||||
func DERPRouteFlag() opt.Bool {
|
||||
v, _ := controlUseDERPRoute.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
if len(routes) == 0 {
|
||||
// Nothing to route, so no need to warn.
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
// We only do subnet routing on Linux for now.
|
||||
// It might work on darwin/macOS when building from source, so
|
||||
// don't return true for other OSes. We can OS-based warnings
|
||||
// already in the admin panel.
|
||||
return false
|
||||
}
|
||||
|
||||
v4Routes, v6Routes := false, false
|
||||
for _, r := range routes {
|
||||
if r.IP.Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
v6Routes = true
|
||||
}
|
||||
}
|
||||
|
||||
if v4Routes {
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
|
||||
if err != nil {
|
||||
// Try another way.
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(string(out)) == "0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if v6Routes {
|
||||
// Note: you might be wondering why we check only the state of
|
||||
// conf.all.forwarding, rather than per-interface forwarding
|
||||
// configuration. According to kernel documentation, it seems
|
||||
// that to actually forward packets, you need to enable
|
||||
// forwarding globally, and the per-interface forwarding
|
||||
// setting only alters other things such as how router
|
||||
// advertisements are handled. The kernel itself warns that
|
||||
// enabling forwarding per-interface and not globally will
|
||||
// probably not work, so I feel okay calling those configs
|
||||
// broken until we have proof otherwise.
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
|
||||
if err != nil {
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv6.conf.all.forwarding").Output()
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(string(out)) == "0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
20
control/controlclient/direct_clone.go
Normal file
20
control/controlclient/direct_clone.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
|
||||
|
||||
package controlclient
|
||||
|
||||
import ()
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
@@ -2,363 +2,92 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build depends_on_currently_unreleased
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.io/control" // not yet released
|
||||
)
|
||||
|
||||
// Test that when there are two controlclient connections using the
|
||||
// same credentials, the later one disconnects the earlier one.
|
||||
func TestClientsReusingKeys(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "control-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
|
||||
return &tailcfg.Node{ID: id, Name: name}
|
||||
}
|
||||
var server *control.Server
|
||||
httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server.ServeHTTP(w, r)
|
||||
}))
|
||||
defer func() {
|
||||
httpsrv.CloseClientConnections()
|
||||
httpsrv.Close()
|
||||
os.RemoveAll(tmpdir)
|
||||
}()
|
||||
|
||||
httpc := httpsrv.Client()
|
||||
httpc.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err = control.New(tmpdir, tmpdir, tmpdir, httpsrv.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.QuietLogging = true
|
||||
|
||||
hi := NewHostinfo()
|
||||
hi.FrontendLogID = "go-test-only"
|
||||
hi.BackendLogID = "go-test-only"
|
||||
|
||||
// Let's test some nonempty extra hostinfo fields to make sure
|
||||
// the server can handle them.
|
||||
hi.RequestTags = []string{"tag:abc"}
|
||||
cidr, err := wgcfg.ParseCIDR("1.2.3.4/24")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCIDR: %v", err)
|
||||
}
|
||||
hi.RoutableIPs = []wgcfg.CIDR{cidr}
|
||||
hi.Services = []tailcfg.Service{
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
Proto: tailcfg.TCP,
|
||||
Port: 1234,
|
||||
Description: "Description",
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
}
|
||||
|
||||
c1, err := NewDirect(Options{
|
||||
ServerURL: httpsrv.URL,
|
||||
HTTPTestClient: httpsrv.Client(),
|
||||
//TimeNow: s.control.TimeNow,
|
||||
Logf: func(fmt string, args ...interface{}) {
|
||||
t.Helper()
|
||||
t.Logf("c1: "+fmt, args...)
|
||||
},
|
||||
Hostinfo: hi,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Use a cancelable context so that goroutines blocking in
|
||||
// PollNetMap shut down when the test exits.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Execute c1's login flow: TryLogin to get an auth URL,
|
||||
// postAuthURL to execute the (faked) OAuth segment of the flow,
|
||||
// and WaitLoginURL to complete the login on the client end.
|
||||
const user = "testuser1@tailscale.onmicrosoft.com"
|
||||
authURL, err := c1.TryLogin(ctx, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
postAuthURL(t, ctx, httpc, user, authURL)
|
||||
newURL, err := c1.WaitLoginURL(ctx, authURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
|
||||
// Start c1's netmap poll in parallel with the rest of the
|
||||
// test. We're expecting it to block happily, invoking the no-op
|
||||
// update function periodically, then exit once c2 starts its own
|
||||
// poll below.
|
||||
gotNetmap := make(chan struct{}, 1)
|
||||
pollErrCh := make(chan error)
|
||||
go func() {
|
||||
pollErrCh <- c1.PollNetMap(ctx, -1, func(netMap *NetworkMap) {
|
||||
select {
|
||||
case gotNetmap <- struct{}{}:
|
||||
default:
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gotNetmap:
|
||||
t.Logf("c1: received initial netmap")
|
||||
case err := <-pollErrCh:
|
||||
t.Fatal(err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("c1 did not receive an initial netmap")
|
||||
}
|
||||
|
||||
// Connect c2, reusing c1's credentials. In other words, c2 *is*
|
||||
// c1 from the server's perspective.
|
||||
c2, err := NewDirect(Options{
|
||||
ServerURL: httpsrv.URL,
|
||||
HTTPTestClient: httpsrv.Client(),
|
||||
Logf: func(fmt string, args ...interface{}) {
|
||||
t.Helper()
|
||||
t.Logf("c2: "+fmt, args...)
|
||||
},
|
||||
Persist: c1.GetPersist(),
|
||||
Hostinfo: hi,
|
||||
NewDecompressor: func() (Decompressor, error) {
|
||||
return zstd.NewReader(nil)
|
||||
},
|
||||
KeepAlive: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authURL, err = c2.TryLogin(ctx, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// We don't expect to be given an authURL, our credentials from c1
|
||||
// should still be good.
|
||||
if authURL != "" {
|
||||
t.Errorf("unexpected authURL %s", authURL)
|
||||
}
|
||||
|
||||
// Request a single netmap, so this function returns promptly
|
||||
// instead of blocking like c1's PollNetMap.
|
||||
err = c2.PollNetMap(ctx, 1, func(netMap *NetworkMap) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Now that c2 connected and got a netmap, we expect c1's poll to
|
||||
// have exited.
|
||||
select {
|
||||
case err := <-pollErrCh:
|
||||
t.Logf("c1: netmap poll aborted as expected (%v)", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("first client poll failed to close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientsReusingOldKey(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "control-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var server *control.Server
|
||||
httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server.ServeHTTP(w, r)
|
||||
}))
|
||||
httpc := httpsrv.Client()
|
||||
httpc.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err = control.New(tmpdir, tmpdir, tmpdir, httpsrv.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.QuietLogging = true
|
||||
defer func() {
|
||||
httpsrv.CloseClientConnections()
|
||||
httpsrv.Close()
|
||||
os.RemoveAll(tmpdir)
|
||||
}()
|
||||
|
||||
hi := NewHostinfo()
|
||||
hi.FrontendLogID = "go-test-only"
|
||||
hi.BackendLogID = "go-test-only"
|
||||
genOpts := func() Options {
|
||||
return Options{
|
||||
ServerURL: httpsrv.URL,
|
||||
HTTPTestClient: httpc,
|
||||
//TimeNow: s.control.TimeNow,
|
||||
Logf: func(fmt string, args ...interface{}) {
|
||||
t.Helper()
|
||||
t.Logf("c1: "+fmt, args...)
|
||||
},
|
||||
Hostinfo: hi,
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
|
||||
}
|
||||
|
||||
// Login with a new node key. This requires authorization.
|
||||
c1, err := NewDirect(genOpts())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
authURL, err := c1.TryLogin(ctx, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const user = "testuser1@tailscale.onmicrosoft.com"
|
||||
postAuthURL(t, ctx, httpc, user, authURL)
|
||||
newURL, err := c1.WaitLoginURL(ctx, authURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
|
||||
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newPrivKey := func(t *testing.T) wgcfg.PrivateKey {
|
||||
t.Helper()
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// Replace the previous key with a new key.
|
||||
persist1 := c1.GetPersist()
|
||||
persist2 := Persist{
|
||||
PrivateMachineKey: persist1.PrivateMachineKey,
|
||||
OldPrivateNodeKey: persist1.PrivateNodeKey,
|
||||
PrivateNodeKey: newPrivKey(t),
|
||||
}
|
||||
opts := genOpts()
|
||||
opts.Persist = persist2
|
||||
|
||||
c1, err = NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if authURL == "" {
|
||||
t.Fatal("expected authURL for reused oldNodeKey, got none")
|
||||
} else {
|
||||
postAuthURL(t, ctx, httpc, user, authURL)
|
||||
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
}
|
||||
if p := c1.GetPersist(); p.PrivateNodeKey != opts.Persist.PrivateNodeKey {
|
||||
t.Error("unexpected node key change")
|
||||
} else {
|
||||
persist2 = p
|
||||
}
|
||||
|
||||
// Here we simulate a client using using old persistent data.
|
||||
// We use the key we have already replaced as the old node key.
|
||||
// This requires the user to authenticate.
|
||||
persist3 := Persist{
|
||||
PrivateMachineKey: persist1.PrivateMachineKey,
|
||||
OldPrivateNodeKey: persist1.PrivateNodeKey,
|
||||
PrivateNodeKey: newPrivKey(t),
|
||||
}
|
||||
opts = genOpts()
|
||||
opts.Persist = persist3
|
||||
|
||||
c1, err = NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if authURL == "" {
|
||||
t.Fatal("expected authURL for reused oldNodeKey, got none")
|
||||
} else {
|
||||
postAuthURL(t, ctx, httpc, user, authURL)
|
||||
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
}
|
||||
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// At this point, there should only be one node for the machine key
|
||||
// registered as active in the server.
|
||||
mkey := tailcfg.MachineKey(persist1.PrivateMachineKey.Public())
|
||||
nodeIDs, err := server.DB().MachineNodes(mkey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(nodeIDs) != 1 {
|
||||
t.Logf("active nodes for machine key %v:", mkey)
|
||||
for i, nodeID := range nodeIDs {
|
||||
nodeKey := server.DB().NodeKey(nodeID)
|
||||
t.Logf("\tnode %d: id=%v, key=%v", i, nodeID, nodeKey)
|
||||
}
|
||||
t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
|
||||
}
|
||||
|
||||
// Now try the previous node key. It should fail.
|
||||
opts = genOpts()
|
||||
opts.Persist = persist2
|
||||
c1, err = NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// TODO(crawshaw): make this return an actual error.
|
||||
// Have cfgdb track expired keys, and when an expired key is reused
|
||||
// produce an error.
|
||||
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if authURL == "" {
|
||||
t.Fatal("expected authURL for reused nodeKey, got none")
|
||||
} else {
|
||||
postAuthURL(t, ctx, httpc, user, authURL)
|
||||
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
}
|
||||
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if nodeIDs, err := server.DB().MachineNodes(mkey); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(nodeIDs) != 1 {
|
||||
t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -5,80 +5,16 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
func parseIP(host string, defaultBits int) (filter.Net, error) {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.IsUnspecified() {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
} else if ip == nil && host == "*" {
|
||||
// User explicitly requested wildcard dst ip
|
||||
return filter.NetAny, nil
|
||||
} else {
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: invalid IPv4 address", host)
|
||||
}
|
||||
return filter.Net{
|
||||
IP: filter.NewIP(ip),
|
||||
Mask: filter.Netmask(defaultBits),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
mm := make([]filter.Match, 0, len(pf))
|
||||
var erracc error
|
||||
|
||||
for _, r := range pf {
|
||||
m := filter.Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
}
|
||||
net, err := parseIP(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Srcs = append(m.Srcs, net)
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
net, err := parseIP(d.IP, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Dsts = append(m.Dsts, filter.NetPortRange{
|
||||
Net: net,
|
||||
Ports: filter.PortRange{
|
||||
First: d.Ports.First,
|
||||
Last: d.Ports.Last,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
|
||||
if erracc != nil {
|
||||
c.logf("parsePacketFilter: %s\n", erracc)
|
||||
// Parse a backward-compatible FilterRule used by control's wire
|
||||
// format, producing the most current filter format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
|
||||
103
control/controlclient/hostinfo_linux.go
Normal file
103
control/controlclient/hostinfo_linux.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux,!android
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionLinux
|
||||
}
|
||||
|
||||
func osVersionLinux() string {
|
||||
dist := distro.Get()
|
||||
propFile := "/etc/os-release"
|
||||
switch dist {
|
||||
case distro.Synology:
|
||||
propFile = "/etc.defaults/VERSION"
|
||||
case distro.OpenWrt:
|
||||
propFile = "/etc/openwrt_release"
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
lineread.File(propFile, func(line []byte) error {
|
||||
eq := bytes.IndexByte(line, '=')
|
||||
if eq == -1 {
|
||||
return nil
|
||||
}
|
||||
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
|
||||
m[k] = v
|
||||
return nil
|
||||
})
|
||||
|
||||
var un syscall.Utsname
|
||||
syscall.Uname(&un)
|
||||
|
||||
var attrBuf strings.Builder
|
||||
attrBuf.WriteString("; kernel=")
|
||||
for _, b := range un.Release {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
|
||||
id := m["ID"]
|
||||
|
||||
switch id {
|
||||
case "debian":
|
||||
slurp, _ := ioutil.ReadFile("/etc/debian_version")
|
||||
return fmt.Sprintf("Debian %s (%s)%s", bytes.TrimSpace(slurp), m["VERSION_CODENAME"], attr)
|
||||
case "ubuntu":
|
||||
return fmt.Sprintf("Ubuntu %s%s", m["VERSION"], attr)
|
||||
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
|
||||
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
|
||||
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
|
||||
}
|
||||
fallthrough
|
||||
case "fedora", "rhel", "alpine", "nixos":
|
||||
// Their PRETTY_NAME is fine as-is for all versions I tested.
|
||||
fallthrough
|
||||
default:
|
||||
if v := m["PRETTY_NAME"]; v != "" {
|
||||
return fmt.Sprintf("%s%s", v, attr)
|
||||
}
|
||||
}
|
||||
switch dist {
|
||||
case distro.Synology:
|
||||
return fmt.Sprintf("Synology %s%s", m["productversion"], attr)
|
||||
case distro.OpenWrt:
|
||||
return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr)
|
||||
}
|
||||
return fmt.Sprintf("Other%s", attr)
|
||||
}
|
||||
|
||||
func inContainer() (ret bool) {
|
||||
lineread.File("/proc/1/cgroup", func(line []byte) error {
|
||||
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
|
||||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
|
||||
ret = true
|
||||
return io.EOF // arbitrary non-nil error to stop loop
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
30
control/controlclient/hostinfo_windows.go
Normal file
30
control/controlclient/hostinfo_windows.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionWindows
|
||||
}
|
||||
|
||||
func osVersionWindows() string {
|
||||
cmd := exec.Command("cmd", "/c", "ver")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
|
||||
s := strings.TrimSpace(string(out))
|
||||
s = strings.TrimPrefix(s, "Microsoft Windows [")
|
||||
s = strings.TrimSuffix(s, "]")
|
||||
|
||||
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
|
||||
if sp := strings.Index(s, " "); sp != -1 {
|
||||
s = s[sp+1:]
|
||||
}
|
||||
return s // "10.0.19041.388", ideally
|
||||
}
|
||||
@@ -5,33 +5,45 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
Expiry time.Time
|
||||
Addresses []wgcfg.CIDR
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []netaddr.IPPrefix
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
Peers []*tailcfg.Node
|
||||
DNS []wgcfg.IP
|
||||
DNSDomains []string
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS tailcfg.DNSConfig
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
PacketFilter filter.Matches
|
||||
PacketFilter []filter.Match
|
||||
|
||||
// DERPMap is the last DERP server map received. It's reused
|
||||
// between updates and should not be modified.
|
||||
DERPMap *tailcfg.DERPMap
|
||||
|
||||
// Debug knobs from control server for debug or feature gating.
|
||||
Debug *tailcfg.Debug
|
||||
|
||||
// ACLs
|
||||
|
||||
@@ -40,97 +52,167 @@ type NetworkMap struct {
|
||||
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
|
||||
// There are lots of ways to slice this data, leave it up to users.
|
||||
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
|
||||
Roles []tailcfg.Role
|
||||
// TODO(crawshaw): Groups []tailcfg.Group
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
func (n *NetworkMap) Equal(n2 *NetworkMap) bool {
|
||||
// TODO(crawshaw): this is crude, but is an easy way to avoid bugs.
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b2, err := json.Marshal(n2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bytes.Equal(b, b2)
|
||||
}
|
||||
|
||||
func (nm NetworkMap) String() string {
|
||||
return nm.Concise()
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) Concise() string {
|
||||
buf := new(strings.Builder)
|
||||
fmt.Fprintf(buf, "netmap: self: %v auth=%v :%v %v\n",
|
||||
nm.NodeKey.ShortString(), nm.MachineStatus,
|
||||
nm.LocalPort, nm.Addresses)
|
||||
|
||||
nm.printConciseHeader(buf)
|
||||
for _, p := range nm.Peers {
|
||||
aip := make([]string, len(p.AllowedIPs))
|
||||
for i, a := range p.AllowedIPs {
|
||||
s := fmt.Sprint(a)
|
||||
if strings.HasSuffix(s, "/32") {
|
||||
s = s[0 : len(s)-3]
|
||||
}
|
||||
aip[i] = s
|
||||
}
|
||||
|
||||
ep := make([]string, len(p.Endpoints))
|
||||
for i, e := range p.Endpoints {
|
||||
// Align vertically on the ':' between IP and port
|
||||
colon := strings.IndexByte(e, ':')
|
||||
for colon > 0 && len(e)-colon < 6 {
|
||||
e += " "
|
||||
colon--
|
||||
}
|
||||
ep[i] = fmt.Sprintf("%21v", e)
|
||||
}
|
||||
|
||||
derp := p.DERP
|
||||
const derpPrefix = "127.3.3.40:"
|
||||
if strings.HasPrefix(derp, derpPrefix) {
|
||||
derp = "D" + derp[len(derpPrefix):]
|
||||
}
|
||||
|
||||
// Most of the time, aip is just one element, so format the
|
||||
// table to look good in that case. This will also make multi-
|
||||
// subnet nodes stand out visually.
|
||||
fmt.Fprintf(buf, " %v %-2v %-15v : %v\n",
|
||||
p.Key.ShortString(), derp,
|
||||
strings.Join(aip, " "),
|
||||
strings.Join(ep, " "))
|
||||
printPeerConcise(buf, p)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// printConciseHeader prints a concise header line representing nm to buf.
|
||||
//
|
||||
// If this function is changed to access different fields of nm, keep
|
||||
// in equalConciseHeader in sync.
|
||||
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
|
||||
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
|
||||
nm.NodeKey.ShortString(), nm.MachineStatus)
|
||||
login := nm.UserProfiles[nm.User].LoginName
|
||||
if login == "" {
|
||||
if nm.User.IsZero() {
|
||||
login = "?"
|
||||
} else {
|
||||
login = fmt.Sprint(nm.User)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, " u=%s", login)
|
||||
if nm.LocalPort != 0 {
|
||||
fmt.Fprintf(buf, " port=%v", nm.LocalPort)
|
||||
}
|
||||
if nm.Debug != nil {
|
||||
j, _ := json.Marshal(nm.Debug)
|
||||
fmt.Fprintf(buf, " debug=%s", j)
|
||||
}
|
||||
fmt.Fprintf(buf, " %v", nm.Addresses)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
// equalConciseHeader reports whether a and b are equal for the fields
|
||||
// used by printConciseHeader.
|
||||
func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
|
||||
if a.NodeKey != b.NodeKey ||
|
||||
a.MachineStatus != b.MachineStatus ||
|
||||
a.LocalPort != b.LocalPort ||
|
||||
a.User != b.User ||
|
||||
len(a.Addresses) != len(b.Addresses) {
|
||||
return false
|
||||
}
|
||||
for i, a := range a.Addresses {
|
||||
if b.Addresses[i] != a {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (a.Debug == nil && b.Debug == nil) || reflect.DeepEqual(a.Debug, b.Debug)
|
||||
}
|
||||
|
||||
// printPeerConcise appends to buf a line repsenting the peer p.
|
||||
//
|
||||
// If this function is changed to access different fields of p, keep
|
||||
// in nodeConciseEqual in sync.
|
||||
func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
aip := make([]string, len(p.AllowedIPs))
|
||||
for i, a := range p.AllowedIPs {
|
||||
s := strings.TrimSuffix(fmt.Sprint(a), "/32")
|
||||
aip[i] = s
|
||||
}
|
||||
|
||||
ep := make([]string, len(p.Endpoints))
|
||||
for i, e := range p.Endpoints {
|
||||
// Align vertically on the ':' between IP and port
|
||||
colon := strings.IndexByte(e, ':')
|
||||
spaces := 0
|
||||
for colon > 0 && len(e)+spaces-colon < 6 {
|
||||
spaces++
|
||||
colon--
|
||||
}
|
||||
ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
|
||||
}
|
||||
|
||||
derp := p.DERP
|
||||
const derpPrefix = "127.3.3.40:"
|
||||
if strings.HasPrefix(derp, derpPrefix) {
|
||||
derp = "D" + derp[len(derpPrefix):]
|
||||
}
|
||||
var discoShort string
|
||||
if !p.DiscoKey.IsZero() {
|
||||
discoShort = p.DiscoKey.ShortString() + " "
|
||||
}
|
||||
|
||||
// Most of the time, aip is just one element, so format the
|
||||
// table to look good in that case. This will also make multi-
|
||||
// subnet nodes stand out visually.
|
||||
fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n",
|
||||
p.Key.ShortString(),
|
||||
discoShort,
|
||||
derp,
|
||||
strings.Join(aip, " "),
|
||||
strings.Join(ep, " "))
|
||||
}
|
||||
|
||||
// nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise.
|
||||
func nodeConciseEqual(a, b *tailcfg.Node) bool {
|
||||
return a.Key == b.Key &&
|
||||
a.DERP == b.DERP &&
|
||||
a.DiscoKey == b.DiscoKey &&
|
||||
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) &&
|
||||
eqStringsIgnoreNil(a.Endpoints, b.Endpoints)
|
||||
}
|
||||
|
||||
func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
|
||||
out := []string{}
|
||||
ra := strings.Split(a.Concise(), "\n")
|
||||
rb := strings.Split(b.Concise(), "\n")
|
||||
var diff strings.Builder
|
||||
|
||||
ma := map[string]struct{}{}
|
||||
for _, s := range ra {
|
||||
ma[s] = struct{}{}
|
||||
// See if header (non-peers, "bare") part of the network map changed.
|
||||
// If so, print its diff lines first.
|
||||
if !a.equalConciseHeader(b) {
|
||||
diff.WriteByte('-')
|
||||
a.printConciseHeader(&diff)
|
||||
diff.WriteByte('+')
|
||||
b.printConciseHeader(&diff)
|
||||
}
|
||||
|
||||
mb := map[string]struct{}{}
|
||||
for _, s := range rb {
|
||||
mb[s] = struct{}{}
|
||||
}
|
||||
|
||||
for _, s := range ra {
|
||||
if _, ok := mb[s]; !ok {
|
||||
out = append(out, "-"+s)
|
||||
aps, bps := a.Peers, b.Peers
|
||||
for len(aps) > 0 && len(bps) > 0 {
|
||||
pa, pb := aps[0], bps[0]
|
||||
switch {
|
||||
case pa.ID == pb.ID:
|
||||
if !nodeConciseEqual(pa, pb) {
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
diff.WriteByte('+')
|
||||
printPeerConcise(&diff, pb)
|
||||
}
|
||||
aps, bps = aps[1:], bps[1:]
|
||||
case pa.ID > pb.ID:
|
||||
// New peer in b.
|
||||
diff.WriteByte('+')
|
||||
printPeerConcise(&diff, pb)
|
||||
bps = bps[1:]
|
||||
case pb.ID > pa.ID:
|
||||
// Deleted peer in b.
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
aps = aps[1:]
|
||||
}
|
||||
}
|
||||
for _, s := range rb {
|
||||
if _, ok := ma[s]; !ok {
|
||||
out = append(out, "+"+s)
|
||||
}
|
||||
for _, pa := range aps {
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
for _, pb := range bps {
|
||||
diff.WriteByte('+')
|
||||
printPeerConcise(&diff, pb)
|
||||
}
|
||||
return diff.String()
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) JSON() string {
|
||||
@@ -141,138 +223,138 @@ func (nm *NetworkMap) JSON() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
const (
|
||||
UAllowSingleHosts = 1 << iota
|
||||
UAllowSubnetRoutes
|
||||
UAllowDefaultRoute
|
||||
UHackDefaultRoute
|
||||
// WGConfigFlags is a bitmask of flags to control the behavior of the
|
||||
// wireguard configuration generation done by NetMap.WGCfg.
|
||||
type WGConfigFlags int
|
||||
|
||||
UDefault = 0
|
||||
const (
|
||||
AllowSingleHosts WGConfigFlags = 1 << iota
|
||||
AllowSubnetRoutes
|
||||
AllowDefaultRoute
|
||||
)
|
||||
|
||||
// Several programs need to parse these arguments into uflags, so let's
|
||||
// centralize it here.
|
||||
func UFlagsHelper(uroutes, rroutes, droutes bool) int {
|
||||
uflags := 0
|
||||
if uroutes {
|
||||
uflags |= UAllowSingleHosts
|
||||
}
|
||||
if rroutes {
|
||||
uflags |= UAllowSubnetRoutes
|
||||
}
|
||||
if droutes {
|
||||
uflags |= UAllowDefaultRoute
|
||||
}
|
||||
return uflags
|
||||
}
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
|
||||
// This form is then recognize by magicsock's CreateEndpoint.
|
||||
const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
|
||||
// TODO(bradfitz): UAPI seems to only be used by the old confnode and
|
||||
// pingnode; delete this when those are deleted/rewritten?
|
||||
func (nm *NetworkMap) UAPI(uflags int, dnsOverride []wgcfg.IP) string {
|
||||
wgcfg, err := nm.WGCfg(uflags, dnsOverride)
|
||||
if err != nil {
|
||||
log.Fatalf("WGCfg() failed unexpectedly: %v\n", err)
|
||||
// WGCfg returns the NetworkMaps's Wireguard configuration.
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
s, err := wgcfg.ToUAPI()
|
||||
if err != nil {
|
||||
log.Fatalf("ToUAPI() failed unexpectedly: %v\n", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) WGCfg(uflags int, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
|
||||
s := nm._WireGuardConfig(uflags, dnsOverride, true)
|
||||
return wgcfg.FromWgQuick(s, "tailscale")
|
||||
}
|
||||
|
||||
// TODO(apenwarr): This mode is dangerous.
|
||||
// Discarding the extra endpoints is almost universally the wrong choice.
|
||||
// Except that plain wireguard can't handle a peer with multiple endpoints.
|
||||
// (Yet?)
|
||||
func (nm *NetworkMap) WireGuardConfigOneEndpoint(uflags int, dnsOverride []wgcfg.IP) string {
|
||||
return nm._WireGuardConfig(uflags, dnsOverride, false)
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) _WireGuardConfig(uflags int, dnsOverride []wgcfg.IP, allEndpoints bool) string {
|
||||
buf := new(strings.Builder)
|
||||
fmt.Fprintf(buf, "[Interface]\n")
|
||||
fmt.Fprintf(buf, "PrivateKey = %s\n", base64.StdEncoding.EncodeToString(nm.PrivateKey[:]))
|
||||
if len(nm.Addresses) > 0 {
|
||||
fmt.Fprintf(buf, "Address = ")
|
||||
for i, cidr := range nm.Addresses {
|
||||
if i > 0 {
|
||||
fmt.Fprintf(buf, ", ")
|
||||
}
|
||||
fmt.Fprintf(buf, "%s", cidr)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
fmt.Fprintf(buf, "ListenPort = %d\n", nm.LocalPort)
|
||||
if len(dnsOverride) > 0 {
|
||||
dnss := []string{}
|
||||
for _, ip := range dnsOverride {
|
||||
dnss = append(dnss, ip.String())
|
||||
}
|
||||
fmt.Fprintf(buf, "DNS = %s\n", strings.Join(dnss, ","))
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
|
||||
for i, peer := range nm.Peers {
|
||||
if (uflags&UAllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
|
||||
log.Printf("wgcfg: %v skipping a single-host peer.\n", peer.Key.ShortString())
|
||||
for _, peer := range nm.Peers {
|
||||
if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
fmt.Fprintf(buf, "\n")
|
||||
if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
|
||||
logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(buf, "[Peer]\n")
|
||||
fmt.Fprintf(buf, "PublicKey = %s\n", base64.StdEncoding.EncodeToString(peer.Key[:]))
|
||||
var endpoints []string
|
||||
if peer.DERP != "" {
|
||||
endpoints = append(endpoints, peer.DERP)
|
||||
}
|
||||
endpoints = append(endpoints, peer.Endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
if len(endpoints) == 1 {
|
||||
fmt.Fprintf(buf, "Endpoint = %s", endpoints[0])
|
||||
} else if allEndpoints {
|
||||
// TODO(apenwarr): This mode is incompatible.
|
||||
// Normal wireguard clients don't know how to
|
||||
// parse it (yet?)
|
||||
fmt.Fprintf(buf, "Endpoint = %s",
|
||||
strings.Join(endpoints, ","))
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Endpoint = %s # other endpoints: %s",
|
||||
endpoints[0],
|
||||
strings.Join(endpoints[1:], ", "))
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
var aips []string
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
aip := allowedIP.String()
|
||||
if allowedIP.Mask == 0 {
|
||||
if (uflags & UAllowDefaultRoute) == 0 {
|
||||
log.Printf("wgcfg: %v skipping default route\n", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
if (uflags & UHackDefaultRoute) != 0 {
|
||||
aip = "10.0.0.0/8"
|
||||
log.Printf("wgcfg: %v converting default route => %v\n", peer.Key.ShortString(), aip)
|
||||
}
|
||||
} else if allowedIP.Mask < 32 {
|
||||
if (uflags & UAllowSubnetRoutes) == 0 {
|
||||
log.Printf("wgcfg: %v skipping subnet route\n", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
aips = append(aips, aip)
|
||||
}
|
||||
fmt.Fprintf(buf, "AllowedIPs = %s\n", strings.Join(aips, ", "))
|
||||
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
|
||||
PublicKey: wgcfg.Key(peer.Key),
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
if peer.KeepAlive {
|
||||
fmt.Fprintf(buf, "PersistentKeepalive = 25\n")
|
||||
cpeer.PersistentKeepalive = 25 // seconds
|
||||
}
|
||||
|
||||
if !peer.DiscoKey.IsZero() {
|
||||
if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpeer.Endpoints = []wgcfg.Endpoint{{Host: fmt.Sprintf("%x.disco.tailscale", peer.DiscoKey[:]), Port: 12345}}
|
||||
} else {
|
||||
if err := appendEndpoint(cpeer, peer.DERP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ep := range peer.Endpoints {
|
||||
if err := appendEndpoint(cpeer, ep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
if cidr.Bits == 0 {
|
||||
return false
|
||||
}
|
||||
if !cidr.IsSingleIP() {
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
if cidr == selfCIDR {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
|
||||
if epStr == "" {
|
||||
return nil
|
||||
}
|
||||
host, port, err := net.SplitHostPort(epStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
peer.Endpoints = append(peer.Endpoints, wgcfg.Endpoint{Host: host, Port: uint16(port16)})
|
||||
return nil
|
||||
}
|
||||
|
||||
// eqStringsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqStringsIgnoreNil(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// eqCIDRsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqCIDRsIgnoreNil(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
297
control/controlclient/netmap_test.go
Normal file
297
control/controlclient/netmap_test.go
Normal file
@@ -0,0 +1,297 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func testNodeKey(b byte) (ret tailcfg.NodeKey) {
|
||||
for i := range ret {
|
||||
ret[i] = b
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func testDiscoKey(hexPrefix string) (ret tailcfg.DiscoKey) {
|
||||
b, err := hex.DecodeString(hexPrefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
copy(ret[:], b)
|
||||
return
|
||||
}
|
||||
|
||||
func TestNetworkMapConcise(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
nm *NetworkMap
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
{
|
||||
Key: testNodeKey(3),
|
||||
DERP: "127.3.3.40:4",
|
||||
Endpoints: []string{"10.2.0.100:12", "10.1.0.100:12345"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
|
||||
},
|
||||
{
|
||||
name: "debug_non_nil",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={} []\n",
|
||||
},
|
||||
{
|
||||
name: "debug_values",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{LogHeapPprof: true},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={\"LogHeapPprof\":true} []\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
n := int(testing.AllocsPerRun(1000, func() {
|
||||
got = tt.nm.Concise()
|
||||
}))
|
||||
t.Logf("Allocs = %d", n)
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrong output\n Got: %q\nWant: %q\n## Got (unescaped):\n%s\n## Want (unescaped):\n%s\n", got, tt.want, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConciseDiffFrom(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
a, b *NetworkMap
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "header_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(2),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n",
|
||||
},
|
||||
{
|
||||
name: "peer_add",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
Key: testNodeKey(1),
|
||||
DERP: "127.3.3.40:1",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Key: testNodeKey(3),
|
||||
DERP: "127.3.3.40:3",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "+ [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n+ [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
|
||||
},
|
||||
{
|
||||
name: "peer_remove",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
Key: testNodeKey(1),
|
||||
DERP: "127.3.3.40:1",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Key: testNodeKey(3),
|
||||
DERP: "127.3.3.40:3",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "- [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n- [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
|
||||
},
|
||||
{
|
||||
name: "peer_port_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "- [AgICA] D2 : 192.168.0.100:12 1.1.1.1:1 \n+ [AgICA] D2 : 192.168.0.100:12 1.1.1.1:2 \n",
|
||||
},
|
||||
{
|
||||
name: "disco_key_only_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "- [AgICA] d:f00f00f00f000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n+ [AgICA] d:ba4ba4ba4b000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
n := int(testing.AllocsPerRun(50, func() {
|
||||
got = tt.b.ConciseDiffFrom(tt.a)
|
||||
}))
|
||||
t.Logf("Allocs = %d", n)
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrong output\n Got: %q\nWant: %q\n## Got (unescaped):\n%s\n## Want (unescaped):\n%s\n", got, tt.want, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
@@ -8,18 +8,18 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
persistHandles := []string{"PrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
||||
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
||||
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
|
||||
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, persistHandles)
|
||||
}
|
||||
|
||||
newPrivate := func() wgcfg.PrivateKey {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
newPrivate := func() wgkey.Private {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -36,13 +36,13 @@ func TestPersistEqual(t *testing.T) {
|
||||
{&Persist{}, &Persist{}, true},
|
||||
|
||||
{
|
||||
&Persist{PrivateMachineKey: k1},
|
||||
&Persist{PrivateMachineKey: newPrivate()},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: newPrivate()},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Persist{PrivateMachineKey: k1},
|
||||
&Persist{PrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
true,
|
||||
},
|
||||
|
||||
|
||||
70
derp/derp.go
70
derp/derp.go
@@ -32,20 +32,17 @@ const MaxPacketSize = 64 << 10
|
||||
const magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
|
||||
|
||||
const (
|
||||
nonceLen = 24
|
||||
keyLen = 32
|
||||
maxInfoLen = 1 << 20
|
||||
keepAlive = 60 * time.Second
|
||||
nonceLen = 24
|
||||
frameHeaderLen = 1 + 4 // frameType byte + 4 byte length
|
||||
keyLen = 32
|
||||
maxInfoLen = 1 << 20
|
||||
keepAlive = 60 * time.Second
|
||||
)
|
||||
|
||||
// protocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// ProtocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// * version 1 (zero on wire): consistent box headers, in use by employee dev nodes a bit
|
||||
// * version 2: received packets have src addrs in frameRecvPacket at beginning
|
||||
const protocolVersion = 2
|
||||
|
||||
const (
|
||||
protocolSrcAddrs = 2 // protocol version at which client expects src addresses
|
||||
)
|
||||
const ProtocolVersion = 2
|
||||
|
||||
// frameType is the one byte frame type at the beginning of the frame
|
||||
// header. The second field is a big-endian uint32 describing the
|
||||
@@ -71,6 +68,7 @@ const (
|
||||
frameClientInfo = frameType(0x02) // 32B pub key + 24B nonce + naclbox(json)
|
||||
frameServerInfo = frameType(0x03) // 24B nonce + naclbox(json)
|
||||
frameSendPacket = frameType(0x04) // 32B dest pub key + packet bytes
|
||||
frameForwardPacket = frameType(0x0a) // 32B src pub key + 32B dst pub key + packet bytes
|
||||
frameRecvPacket = frameType(0x05) // v0/1: packet bytes, v2: 32B src pub key + packet bytes
|
||||
frameKeepAlive = frameType(0x06) // no payload, no-op (to be replaced with ping/pong)
|
||||
frameNotePreferred = frameType(0x07) // 1 byte payload: 0x01 or 0x00 for whether this is client's home node
|
||||
@@ -81,6 +79,24 @@ const (
|
||||
// framePeerGone to B so B can forget that a reverse path
|
||||
// exists on that connection to get back to A.
|
||||
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone
|
||||
|
||||
// framePeerPresent is like framePeerGone, but for other
|
||||
// members of the DERP region when they're meshed up together.
|
||||
framePeerPresent = frameType(0x09) // 32B pub key of peer that's connected
|
||||
|
||||
// frameWatchConns is how one DERP node in a regional mesh
|
||||
// subscribes to the others in the region.
|
||||
// There's no payload. If the sender doesn't have permission, the connection
|
||||
// is closed. Otherwise, the client is initially flooded with
|
||||
// framePeerPresent for all connected nodes, and then a stream of
|
||||
// framePeerPresent & framePeerGone has peers connect and disconnect.
|
||||
frameWatchConns = frameType(0x10)
|
||||
|
||||
// frameClosePeer is a privileged frame type (requires the
|
||||
// mesh key for now) that closes the provided peer's
|
||||
// connection. (To be used for cluster load balancing
|
||||
// purposes, when clients end up on a non-ideal node)
|
||||
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
@@ -88,16 +104,31 @@ var bin = binary.BigEndian
|
||||
func writeUint32(bw *bufio.Writer, v uint32) error {
|
||||
var b [4]byte
|
||||
bin.PutUint32(b[:], v)
|
||||
_, err := bw.Write(b[:])
|
||||
return err
|
||||
// Writing a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for _, c := range &b {
|
||||
err := bw.WriteByte(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUint32(br *bufio.Reader) (uint32, error) {
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(br, b); err != nil {
|
||||
return 0, err
|
||||
var b [4]byte
|
||||
// Reading a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for i := range &b {
|
||||
c, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
b[i] = c
|
||||
}
|
||||
return bin.Uint32(b), nil
|
||||
return bin.Uint32(b[:]), nil
|
||||
}
|
||||
|
||||
func readFrameTypeHeader(br *bufio.Reader, wantType frameType) (frameLen uint32, err error) {
|
||||
@@ -174,13 +205,6 @@ func writeFrame(bw *bufio.Writer, t frameType, b []byte) error {
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func minUint32(a, b uint32) uint32 {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@@ -19,21 +19,63 @@ import (
|
||||
"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
|
||||
protoVersion int // min of server+client
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
|
||||
// Owned by Recv:
|
||||
peeked int // bytes to discard on next Recv
|
||||
readErr error // sticky read error
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf) (*Client, error) {
|
||||
// ClientOpt is an option passed to NewClient.
|
||||
type ClientOpt interface {
|
||||
update(*clientOpt)
|
||||
}
|
||||
|
||||
type clientOptFunc func(*clientOpt)
|
||||
|
||||
func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
// access to join the mesh.
|
||||
//
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
|
||||
// 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 {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
if o == nil {
|
||||
return nil, errors.New("nil ClientOpt")
|
||||
}
|
||||
o.update(&opt)
|
||||
}
|
||||
return newClient(privateKey, nc, brw, logf, opt)
|
||||
}
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
@@ -41,19 +83,18 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
}
|
||||
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
}
|
||||
} else {
|
||||
c.serverKey = opt.ServerPub
|
||||
}
|
||||
if err := c.sendClientKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
|
||||
}
|
||||
info, err := c.recvServerInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server info: %v", err)
|
||||
}
|
||||
c.protoVersion = minInt(protocolVersion, info.Version)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -74,12 +115,9 @@ func (c *Client) recvServerKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
fl, err := readFrameTypeHeader(c.br, frameServerInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
const maxLength = nonceLen + maxInfoLen
|
||||
fl := len(b)
|
||||
if fl < nonceLen {
|
||||
return nil, fmt.Errorf("short serverInfo frame")
|
||||
}
|
||||
@@ -88,27 +126,27 @@ func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
}
|
||||
// TODO: add a read-nonce-and-box helper
|
||||
var nonce [nonceLen]byte
|
||||
if _, err := io.ReadFull(c.br, nonce[:]); err != nil {
|
||||
return nil, fmt.Errorf("nonce: %v", err)
|
||||
}
|
||||
msgLen := fl - nonceLen
|
||||
msgbox := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(c.br, msgbox); err != nil {
|
||||
return nil, fmt.Errorf("msgbox: %v", err)
|
||||
}
|
||||
copy(nonce[:], b)
|
||||
msgbox := b[nonceLen:]
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("msgbox: cannot open len=%d with server key %x", msgLen, c.serverKey[:])
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %x", c.serverKey[:])
|
||||
}
|
||||
info := new(serverInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
return nil, fmt.Errorf("msg: %v", err)
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
Version int // `json:"version,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
// MeshKey optionally specifies a pre-shared key used by
|
||||
// trusted clients. It's required to subscribe to the
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -116,7 +154,10 @@ func (c *Client) sendClientKey() error {
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{Version: protocolVersion})
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -129,6 +170,9 @@ func (c *Client) sendClientKey() error {
|
||||
return writeFrame(c.bw, frameClientInfo, buf)
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
func (c *Client) ServerPublicKey() key.Public { 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.
|
||||
@@ -160,6 +204,40 @@ 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) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.ForwardPacket: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(pkt) > MaxPacketSize {
|
||||
return fmt.Errorf("packet too big: %d", len(pkt))
|
||||
}
|
||||
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, c.writeTimeoutFired)
|
||||
defer timer.Stop()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(srcKey[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
// NotePreferred sends a packet that tells the server whether this
|
||||
// client is the user's preferred server. This is only used in the
|
||||
// server for stats.
|
||||
@@ -186,6 +264,25 @@ func (c *Client) NotePreferred(preferred bool) (err error) {
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
// WatchConnectionChanges sends a request to subscribe to the peer's connection list.
|
||||
// It's a fatal error if the client wasn't created using MeshKey.
|
||||
func (c *Client) WatchConnectionChanges() error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, frameWatchConns, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
return writeFrame(c.bw, frameClosePeer, target[:])
|
||||
}
|
||||
|
||||
// ReceivedMessage represents a type returned by Client.Recv. Unless
|
||||
// otherwise documented, the returned message aliases the byte slice
|
||||
// provided to Recv and thus the message is only as good as that
|
||||
@@ -211,11 +308,28 @@ type PeerGoneMessage key.Public
|
||||
|
||||
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
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
|
||||
// ServerInfoMessage is sent by the server upon first connect.
|
||||
type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
// The provided buffer must be large enough to receive a complete packet,
|
||||
// which in practice are are 1.5-4 KB, but can be up to 64 KB.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
// should only be accessed until the next call to Client.
|
||||
//
|
||||
// Once Recv returns an error, the Client is dead forever.
|
||||
func (c *Client) Recv(b []byte) (m ReceivedMessage, err error) {
|
||||
func (c *Client) Recv() (m ReceivedMessage, err error) {
|
||||
return c.recvTimeout(120 * time.Second)
|
||||
}
|
||||
|
||||
func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err error) {
|
||||
if c.readErr != nil {
|
||||
return nil, c.readErr
|
||||
}
|
||||
@@ -227,14 +341,61 @@ func (c *Client) Recv(b []byte) (m ReceivedMessage, err error) {
|
||||
}()
|
||||
|
||||
for {
|
||||
c.nc.SetReadDeadline(time.Now().Add(120 * time.Second))
|
||||
t, n, err := readFrame(c.br, 1<<20, b)
|
||||
c.nc.SetReadDeadline(time.Now().Add(timeout))
|
||||
|
||||
// Discard any peeked bytes from a previous Recv call.
|
||||
if c.peeked != 0 {
|
||||
if n, err := c.br.Discard(c.peeked); err != nil || n != c.peeked {
|
||||
// Documented to never fail, but might as well check.
|
||||
return nil, fmt.Errorf("bufio.Reader.Discard(%d bytes): got %v, %v", c.peeked, n, err)
|
||||
}
|
||||
c.peeked = 0
|
||||
}
|
||||
|
||||
t, n, err := readFrameHeader(c.br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n > 1<<20 {
|
||||
return nil, fmt.Errorf("unexpectedly large frame of %d bytes returned", n)
|
||||
}
|
||||
|
||||
var b []byte // frame payload (past the 5 byte header)
|
||||
|
||||
// If the frame fits in our bufio.Reader buffer, just use it.
|
||||
// In practice it's 4KB (from derphttp.Client's bufio.NewReader(httpConn)) and
|
||||
// in practive, WireGuard packets (and thus DERP frames) are under 1.5KB.
|
||||
// So this is the common path.
|
||||
if int(n) <= c.br.Size() {
|
||||
b, err = c.br.Peek(int(n))
|
||||
c.peeked = int(n)
|
||||
} else {
|
||||
// But if for some reason we read a large DERP message (which isn't necessarily
|
||||
// a Wireguard packet), then just allocate memory for it.
|
||||
// TODO(bradfitz): use a pool if large frames ever happen in practice.
|
||||
b = make([]byte, n)
|
||||
_, err = io.ReadFull(c.br, b)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch t {
|
||||
default:
|
||||
continue
|
||||
case frameServerInfo:
|
||||
// Server sends this at start-up. Currently unused.
|
||||
// Just has a JSON message saying "version: 2",
|
||||
// but the protocol seems extensible enough as-is without
|
||||
// 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)
|
||||
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
|
||||
case frameKeepAlive:
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
@@ -248,18 +409,23 @@ func (c *Client) Recv(b []byte) (m ReceivedMessage, err error) {
|
||||
copy(pg[:], b[:keyLen])
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerPresentMessage
|
||||
copy(pg[:], b[:keyLen])
|
||||
return pg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
var rp ReceivedPacket
|
||||
if c.protoVersion < protocolSrcAddrs {
|
||||
rp.Data = b[:n]
|
||||
} else {
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,26 +8,49 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newPrivateKey(t *testing.T) (k key.Private) {
|
||||
t.Helper()
|
||||
func newPrivateKey(tb testing.TB) (k key.Private) {
|
||||
tb.Helper()
|
||||
if _, err := crand.Read(k[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
`{"Version":5,"MeshKey":"abc"}`,
|
||||
`{"version":5,"meshKey":"abc"}`,
|
||||
} {
|
||||
var got clientInfo
|
||||
if err := json.Unmarshal([]byte(in), &got); err != nil {
|
||||
t.Fatalf("[%d]: %v", i, err)
|
||||
}
|
||||
want := clientInfo{Version: 5, MeshKey: "abc"}
|
||||
if got != want {
|
||||
t.Errorf("[%d]: got %+v; want %+v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -76,6 +99,8 @@ func TestSendRecv(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
t.Logf("Connected client %d.", i)
|
||||
@@ -87,8 +112,7 @@ func TestSendRecv(t *testing.T) {
|
||||
for i := 0; i < numClients; i++ {
|
||||
go func(i int) {
|
||||
for {
|
||||
b := make([]byte, 1<<16)
|
||||
m, err := clients[i].Recv(b)
|
||||
m, err := clients[i].Recv()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
@@ -103,7 +127,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if m.Source.IsZero() {
|
||||
t.Errorf("zero Source address in ReceivedPacket")
|
||||
}
|
||||
recvChs[i] <- m.Data
|
||||
recvChs[i] <- append([]byte(nil), m.Data...)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
@@ -116,7 +140,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -222,6 +246,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, c2
|
||||
}
|
||||
|
||||
@@ -256,8 +281,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
recv := func(name string, client *Client) {
|
||||
ch := chs(name)
|
||||
for {
|
||||
b := make([]byte, 1<<9)
|
||||
m, err := client.Recv(b)
|
||||
m, err := client.Recv()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("%s: %w", name, err)
|
||||
return
|
||||
@@ -391,3 +415,478 @@ func TestSendFreeze(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testServer struct {
|
||||
s *Server
|
||||
ln net.Listener
|
||||
logf logger.Logf
|
||||
|
||||
mu sync.Mutex
|
||||
pubName map[key.Public]string
|
||||
clients map[*testClient]bool
|
||||
}
|
||||
|
||||
func (ts *testServer) addTestClient(c *testClient) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
ts.clients[c] = true
|
||||
}
|
||||
|
||||
func (ts *testServer) addKeyName(k key.Public, 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 {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
if name, ok := ts.pubName[k]; ok {
|
||||
return name
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
|
||||
func (ts *testServer) close(t *testing.T) error {
|
||||
ts.ln.Close()
|
||||
ts.s.Close()
|
||||
for c := range ts.clients {
|
||||
c.close(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) *testServer {
|
||||
t.Helper()
|
||||
logf := logger.WithPrefix(t.Logf, "derp-server: ")
|
||||
s := NewServer(newPrivateKey(t), logf)
|
||||
s.SetMeshKey("mesh-key")
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
i := 0
|
||||
for {
|
||||
i++
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// TODO: register c in ts so Close also closes it?
|
||||
go func(i int) {
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
|
||||
go s.Accept(c, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
return &testServer{
|
||||
s: s,
|
||||
ln: ln,
|
||||
logf: logf,
|
||||
clients: map[*testClient]bool{},
|
||||
pubName: map[key.Public]string{},
|
||||
}
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
name string
|
||||
c *Client
|
||||
nc net.Conn
|
||||
pub key.Public
|
||||
ts *testServer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.Private, 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+": "))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tc := &testClient{
|
||||
name: name,
|
||||
nc: nc,
|
||||
c: c,
|
||||
ts: ts,
|
||||
pub: key.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) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, nil
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf, MeshKey("mesh-key"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
if err := c.WatchConnectionChanges(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *testClient) wantPresent(t *testing.T, peers ...key.Public) {
|
||||
t.Helper()
|
||||
want := map[key.Public]bool{}
|
||||
for _, k := range peers {
|
||||
want[k] = true
|
||||
}
|
||||
|
||||
for {
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerPresentMessage:
|
||||
got := key.Public(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 {
|
||||
fmt.Fprintf(bw, "%s ", tc.ts.keyName(pub))
|
||||
}
|
||||
}))
|
||||
}
|
||||
delete(want, got)
|
||||
if len(want) == 0 {
|
||||
return
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected message type %T", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *testClient) wantGone(t *testing.T, peer key.Public) {
|
||||
t.Helper()
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerGoneMessage:
|
||||
got := key.Public(m)
|
||||
if peer != got {
|
||||
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected message type %T", m)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testClient) close(t *testing.T) {
|
||||
t.Helper()
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
c.closed = true
|
||||
t.Logf("closing client %q (%x)", c.name, c.pub)
|
||||
c.nc.Close()
|
||||
}
|
||||
|
||||
// TestWatch tests the connection watcher mechanism used by regional
|
||||
// DERP nodes to mesh up with each other.
|
||||
func TestWatch(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.close(t)
|
||||
|
||||
w1 := newTestWatcher(t, ts, "w1")
|
||||
w1.wantPresent(t, w1.pub)
|
||||
|
||||
c1 := newRegularClient(t, ts, "c1")
|
||||
w1.wantPresent(t, c1.pub)
|
||||
|
||||
c2 := newRegularClient(t, ts, "c2")
|
||||
w1.wantPresent(t, c2.pub)
|
||||
|
||||
w2 := newTestWatcher(t, ts, "w2")
|
||||
w1.wantPresent(t, w2.pub)
|
||||
w2.wantPresent(t, w1.pub, w2.pub, c1.pub, c2.pub)
|
||||
|
||||
c3 := newRegularClient(t, ts, "c3")
|
||||
w1.wantPresent(t, c3.pub)
|
||||
w2.wantPresent(t, c3.pub)
|
||||
|
||||
c2.close(t)
|
||||
w1.wantGone(t, c2.pub)
|
||||
w2.wantGone(t, c2.pub)
|
||||
|
||||
w3 := newTestWatcher(t, ts, "w3")
|
||||
w1.wantPresent(t, w3.pub)
|
||||
w2.wantPresent(t, w3.pub)
|
||||
w3.wantPresent(t, c1.pub, c3.pub, w1.pub, w2.pub, w3.pub)
|
||||
|
||||
c1.close(t)
|
||||
w1.wantGone(t, c1.pub)
|
||||
w2.wantGone(t, c1.pub)
|
||||
w3.wantGone(t, c1.pub)
|
||||
}
|
||||
|
||||
type testFwd int
|
||||
|
||||
func (testFwd) ForwardPacket(key.Public, key.Public, []byte) error { panic("not called in tests") }
|
||||
|
||||
func pubAll(b byte) (ret key.Public) {
|
||||
for i := range ret {
|
||||
ret[i] = b
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestForwarderRegistration(t *testing.T) {
|
||||
s := &Server{
|
||||
clients: make(map[key.Public]*sclient),
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
}
|
||||
want := func(want map[key.Public]PacketForwarder) {
|
||||
t.Helper()
|
||||
if got := s.clientsMesh; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("mismatch\n got: %v\nwant: %v\n", got, want)
|
||||
}
|
||||
}
|
||||
wantCounter := func(c *expvar.Int, want int) {
|
||||
t.Helper()
|
||||
if got := c.Value(); got != int64(want) {
|
||||
t.Errorf("counter = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
u1 := pubAll(1)
|
||||
u2 := pubAll(2)
|
||||
u3 := pubAll(3)
|
||||
|
||||
s.AddPacketForwarder(u1, testFwd(1))
|
||||
s.AddPacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]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{
|
||||
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{
|
||||
u1: testFwd(1),
|
||||
u2: testFwd(2),
|
||||
})
|
||||
|
||||
// Actual removal.
|
||||
s.RemovePacketForwarder(u2, testFwd(2))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: testFwd(1),
|
||||
})
|
||||
|
||||
// Adding a dup for a user.
|
||||
wantCounter(&s.multiForwarderCreated, 0)
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
},
|
||||
})
|
||||
wantCounter(&s.multiForwarderCreated, 1)
|
||||
|
||||
// Removing a forwarder in a multi set that doesn't exist; does nothing.
|
||||
s.RemovePacketForwarder(u1, testFwd(55))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
},
|
||||
})
|
||||
|
||||
// Removing a forwarder in a multi set that does exist should collapse it away
|
||||
// from being a multiForwarder.
|
||||
wantCounter(&s.multiForwarderDeleted, 0)
|
||||
s.RemovePacketForwarder(u1, testFwd(1))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: testFwd(100),
|
||||
})
|
||||
wantCounter(&s.multiForwarderDeleted, 1)
|
||||
|
||||
// Removing an entry for a client that's still connected locally should result
|
||||
// in a nil forwarder.
|
||||
u1c := &sclient{
|
||||
key: u1,
|
||||
logf: logger.Discard,
|
||||
}
|
||||
s.clients[u1] = u1c
|
||||
s.RemovePacketForwarder(u1, testFwd(100))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
|
||||
// But once that client disconnects, it should go away.
|
||||
s.unregisterClient(u1c)
|
||||
want(map[key.Public]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{
|
||||
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.clientsMesh[u1] = nil
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: nil,
|
||||
})
|
||||
s.AddPacketForwarder(u1, testFwd(3))
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: testFwd(3),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := newPrivateKey(t)
|
||||
pub := priv.Public()
|
||||
s := NewServer(priv, t.Logf)
|
||||
|
||||
certBytes := s.MetaCert()
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
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 {
|
||||
t.Errorf("CommonName = %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000, 10000} {
|
||||
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
serverPrivateKey := newPrivateKey(b)
|
||||
s := NewServer(serverPrivateKey, logger.Discard)
|
||||
defer s.Close()
|
||||
|
||||
key := newPrivateKey(b)
|
||||
clientKey := key.Public()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
connOut, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer connOut.Close()
|
||||
|
||||
connIn, err := ln.Accept()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer connIn.Close()
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn))
|
||||
go s.Accept(connIn, brwServer, "test-client")
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
|
||||
client, err := NewClient(key, connOut, brw, logger.Discard)
|
||||
if err != nil {
|
||||
b.Fatalf("client: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, err := client.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
msg := make([]byte, packetSize)
|
||||
b.SetBytes(int64(len(msg)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := client.Send(clientKey, msg); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteUint32(b *testing.B) {
|
||||
w := bufio.NewWriter(ioutil.Discard)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writeUint32(w, 0x0ba3a)
|
||||
}
|
||||
}
|
||||
|
||||
type nopRead struct{}
|
||||
|
||||
func (r nopRead) Read(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var sinkU32 uint32
|
||||
|
||||
func BenchmarkReadUint32(b *testing.B) {
|
||||
r := bufio.NewReader(nopRead{})
|
||||
var err error
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkU32, err = readUint32(r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,28 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -40,23 +49,49 @@ import (
|
||||
type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
|
||||
privateKey key.Private
|
||||
logf logger.Logf
|
||||
url *url.URL
|
||||
|
||||
// Either url or getRegion is non-nil:
|
||||
url *url.URL
|
||||
getRegion func() *tailcfg.DERPRegion
|
||||
|
||||
ctx context.Context // closed via cancelCtx in Client.Close
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.Public
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
logf: logf,
|
||||
getRegion: getRegion,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegion method called.
|
||||
// It's used by the netcheck package.
|
||||
func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
return &Client{logf: logf}
|
||||
}
|
||||
|
||||
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection use Connect.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewClient(privateKey key.Private, serverURL string, logf logger.Logf) (*Client, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
@@ -65,6 +100,7 @@ func NewClient(privateKey key.Private, serverURL string, logf logger.Logf) (*Cli
|
||||
if urlPort(u) == "" {
|
||||
return nil, fmt.Errorf("derphttp.NewClient: invalid URL scheme %q", u.Scheme)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
@@ -79,10 +115,20 @@ func NewClient(privateKey key.Private, serverURL string, logf logger.Logf) (*Cli
|
||||
// Connect connects or reconnects to the server, unless already connected.
|
||||
// It returns nil if there was already a good connection, or if one was made.
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
_, err := c.connect(ctx, "derphttp.Client.Connect")
|
||||
_, _, err := c.connect(ctx, "derphttp.Client.Connect")
|
||||
return err
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
//
|
||||
// It only returns a non-zero value once a connection has succeeded
|
||||
// from an earlier call.
|
||||
func (c *Client) ServerPublicKey() key.Public {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.serverPubKey
|
||||
}
|
||||
|
||||
func urlPort(u *url.URL) string {
|
||||
if p := u.Port(); p != "" {
|
||||
return p
|
||||
@@ -96,18 +142,45 @@ func urlPort(u *url.URL) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, err error) {
|
||||
func (c *Client) targetString(reg *tailcfg.DERPRegion) string {
|
||||
if c.url != nil {
|
||||
return c.url.String()
|
||||
}
|
||||
return fmt.Sprintf("region %d (%v)", reg.RegionID, reg.RegionCode)
|
||||
}
|
||||
|
||||
func (c *Client) useHTTPS() bool {
|
||||
if c.url != nil && c.url.Scheme == "http" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tlsServerName returns the tls.Config.ServerName value (for the TLS ClientHello).
|
||||
func (c *Client) tlsServerName(node *tailcfg.DERPNode) string {
|
||||
if c.url != nil {
|
||||
return c.url.Host
|
||||
}
|
||||
return node.HostName
|
||||
}
|
||||
|
||||
func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
if c.url != nil {
|
||||
return c.url.String()
|
||||
}
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, connGen int, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed {
|
||||
return nil, ErrClientClosed
|
||||
return nil, 0, ErrClientClosed
|
||||
}
|
||||
if c.client != nil {
|
||||
return c.client, nil
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
c.logf("%s: connecting to %v", caller, c.url)
|
||||
|
||||
// timeout is the fallback maximum time (if ctx doesn't limit
|
||||
// it further) to do all of: DNS + TCP + TLS + HTTP Upgrade +
|
||||
// DERP upgrade.
|
||||
@@ -127,38 +200,42 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}()
|
||||
defer cancel()
|
||||
|
||||
var reg *tailcfg.DERPRegion // nil when using c.url to dial
|
||||
if c.getRegion != nil {
|
||||
reg = c.getRegion()
|
||||
if reg == nil {
|
||||
return nil, 0, errors.New("DERP region not available")
|
||||
}
|
||||
}
|
||||
|
||||
var tcpConn net.Conn
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
err = fmt.Errorf("%v: %v", ctx.Err(), err)
|
||||
}
|
||||
err = fmt.Errorf("%s connect to %v: %v", caller, c.url, err)
|
||||
err = fmt.Errorf("%s connect to %v: %v", caller, c.targetString(reg), err)
|
||||
if tcpConn != nil {
|
||||
go tcpConn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
host := c.url.Hostname()
|
||||
hostOrIP := host
|
||||
|
||||
var d net.Dialer
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostOrIP = ip.String()
|
||||
var node *tailcfg.DERPNode // nil when using c.url to dial
|
||||
if c.url != nil {
|
||||
c.logf("%s: connecting to %v", caller, c.url)
|
||||
tcpConn, err = c.dialURL(ctx)
|
||||
} else {
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
}
|
||||
|
||||
tcpConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(hostOrIP, urlPort(c.url)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial of %q: %v", host, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Now that we have a TCP connection, force close it.
|
||||
// Now that we have a TCP connection, force close it if the
|
||||
// TLS handshake + DERP setup takes too long.
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
@@ -181,57 +258,370 @@ 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
|
||||
if c.url.Scheme == "https" {
|
||||
httpConn = tls.Client(tcpConn, tlsdial.Config(c.url.Host, c.TLSConfig))
|
||||
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 serverProtoVersion int
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
httpConn = tlsConn
|
||||
|
||||
// Force a handshake now (instead of waiting for it to
|
||||
// be done implicitly on read/write) so we can check
|
||||
// the ConnectionState.
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// We expect to be using TLS 1.3 to our own servers, and only
|
||||
// starting at TLS 1.3 are the server's returned certificates
|
||||
// encrypted, so only look for and use our "meta cert" if we're
|
||||
// using TLS 1.3. If we're not using TLS 1.3, it might be a user
|
||||
// running cmd/derper themselves with a different configuration,
|
||||
// in which case we can avoid this fast-start optimization.
|
||||
// (If a corporate proxy is MITM'ing TLS 1.3 connections with
|
||||
// corp-mandated TLS root certs than all bets are off anyway.)
|
||||
// Note that we're not specifically concerned about TLS downgrade
|
||||
// attacks. TLS handles that fine:
|
||||
// https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html
|
||||
connState := tlsConn.ConnectionState()
|
||||
if connState.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates)
|
||||
}
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
}
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(httpConn), bufio.NewWriter(httpConn))
|
||||
var derpClient *derp.Client
|
||||
|
||||
req, err := http.NewRequest("GET", c.url.String(), nil)
|
||||
req, err := http.NewRequest("GET", c.urlString(node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Upgrade", "DERP")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !serverPub.IsZero() && serverProtoVersion != 0 {
|
||||
// parseMetaCert found the server's public key (no TLS
|
||||
// middlebox was in the way), so skip the HTTP upgrade
|
||||
// exchange. See https://github.com/tailscale/tailscale/issues/693
|
||||
// for an overview. We still send the HTTP request
|
||||
// just to get routed into the server's HTTP Handler so it
|
||||
// can Hijack the request, but we signal with a special header
|
||||
// that we don't want to deal with its HTTP response.
|
||||
req.Header.Set(fastStartHeader, "1") // suppresses the server's HTTP response
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// No need to flush the HTTP request. the derp.Client's initial
|
||||
// client auth frame will flush it.
|
||||
} else {
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
|
||||
derpClient, err := derp.NewClient(c.privateKey, httpConn, brw, c.logf)
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
if c.preferred {
|
||||
if err := derpClient.NotePreferred(true); err != nil {
|
||||
go httpConn.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
host := c.url.Hostname()
|
||||
hostOrIP := host
|
||||
|
||||
dialer := netns.NewDialer()
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err == nil {
|
||||
hostOrIP = ip.String()
|
||||
}
|
||||
if err != nil && netns.IsSOCKSDialer(dialer) {
|
||||
// Return an error if we're not using a dial
|
||||
// proxy that can do DNS lookups for us.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
return c.client, nil
|
||||
tcpConn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(hostOrIP, urlPort(c.url)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial of %v: %v", host, err)
|
||||
}
|
||||
return tcpConn, nil
|
||||
}
|
||||
|
||||
// dialRegion returns a TCP connection to the provided region, trying
|
||||
// each node in order (with dialNode) until one connects or ctx is
|
||||
// done.
|
||||
func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.Conn, *tailcfg.DERPNode, error) {
|
||||
if len(reg.Nodes) == 0 {
|
||||
return nil, nil, fmt.Errorf("no nodes for %s", c.targetString(reg))
|
||||
}
|
||||
var firstErr error
|
||||
for _, n := range reg.Nodes {
|
||||
if n.STUNOnly {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("no non-STUNOnly nodes for %s", c.targetString(reg))
|
||||
}
|
||||
continue
|
||||
}
|
||||
c, err := c.dialNode(ctx, n)
|
||||
if err == nil {
|
||||
return c, n, nil
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return nil, nil, firstErr
|
||||
}
|
||||
|
||||
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig)
|
||||
if node != nil {
|
||||
if node.DERPTestPort != 0 {
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, err error) {
|
||||
tcpConn, node, err := c.dialRegion(ctx, reg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
done := make(chan bool) // unbufferd
|
||||
defer close(done)
|
||||
|
||||
tlsConn = c.tlsClient(tcpConn, node)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-ctx.Done():
|
||||
tcpConn.Close()
|
||||
}
|
||||
}()
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
select {
|
||||
case done <- true:
|
||||
return tlsConn, tcpConn, nil
|
||||
case <-ctx.Done():
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
|
||||
return netns.NewDialer().DialContext(ctx, proto, addr)
|
||||
}
|
||||
|
||||
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
|
||||
// address (given in s) is valid. An empty value means to dial, but to
|
||||
// use DNS. The predicate function reports whether the non-empty
|
||||
// string s contained a valid IP address of the right family.
|
||||
func shouldDialProto(s string, pred func(netaddr.IP) bool) bool {
|
||||
if s == "" {
|
||||
return true
|
||||
}
|
||||
ip, _ := netaddr.ParseIP(s)
|
||||
return pred(ip)
|
||||
}
|
||||
|
||||
const dialNodeTimeout = 1500 * time.Millisecond
|
||||
|
||||
// dialNode returns a TCP connection to node n, racing IPv4 and IPv6
|
||||
// (both as applicable) against each other.
|
||||
// A node is only given dialNodeTimeout to connect.
|
||||
//
|
||||
// TODO(bradfitz): longer if no options remain perhaps? ... Or longer
|
||||
// overall but have dialRegion start overlapping races?
|
||||
func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) {
|
||||
// First see if we need to use an HTTP proxy.
|
||||
proxyReq := &http.Request{
|
||||
Method: "GET", // doesn't really matter
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: c.tlsServerName(n),
|
||||
Path: "/", // unused
|
||||
},
|
||||
}
|
||||
if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
}
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
err error
|
||||
}
|
||||
resc := make(chan res) // must be unbuffered
|
||||
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
|
||||
defer cancel()
|
||||
|
||||
nwait := 0
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
nwait++
|
||||
go func() {
|
||||
dst := dstPrimary
|
||||
if dst == "" {
|
||||
dst = n.HostName
|
||||
}
|
||||
port := "443"
|
||||
if n.DERPTestPort != 0 {
|
||||
port = fmt.Sprint(n.DERPTestPort)
|
||||
}
|
||||
c, err := c.dialContext(ctx, proto, net.JoinHostPort(dst, port))
|
||||
select {
|
||||
case resc <- res{c, err}:
|
||||
case <-ctx.Done():
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
if shouldDialProto(n.IPv4, netaddr.IP.Is4) {
|
||||
startDial(n.IPv4, "tcp4")
|
||||
}
|
||||
if shouldDialProto(n.IPv6, netaddr.IP.Is6) {
|
||||
startDial(n.IPv6, "tcp6")
|
||||
}
|
||||
if nwait == 0 {
|
||||
return nil, errors.New("both IPv4 and IPv6 are explicitly disabled for node")
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for {
|
||||
select {
|
||||
case res := <-resc:
|
||||
nwait--
|
||||
if res.err == nil {
|
||||
return res.c, nil
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = res.err
|
||||
}
|
||||
if nwait == 0 {
|
||||
return nil, firstErr
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func firstStr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
|
||||
func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (proxyConn net.Conn, err error) {
|
||||
pu := proxyURL
|
||||
if pu.Scheme == "https" {
|
||||
var d tls.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443")))
|
||||
} else {
|
||||
var d net.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80")))
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && proxyConn != nil {
|
||||
// In a goroutine in case it's a *tls.Conn (that can block on Close)
|
||||
// TODO(bradfitz): track the underlying tcp.Conn and just close that instead.
|
||||
go proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
target := net.JoinHostPort(n.HostName, "443")
|
||||
|
||||
var authHeader string
|
||||
if v, err := tshttpproxy.GetAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, pu.Hostname(), authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(proxyConn)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, err)
|
||||
return nil, err
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, res.Status)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status)
|
||||
}
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
client, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,6 +631,17 @@ func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.ForwardPacket(from, to, b); err != nil {
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NotePreferred notes whether this Client is the caller's preferred
|
||||
// (home) DERP node. It's only used for stats.
|
||||
func (c *Client) NotePreferred(v bool) {
|
||||
@@ -260,18 +661,58 @@ func (c *Client) NotePreferred(v bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Recv(b []byte) (derp.ReceivedMessage, error) {
|
||||
client, err := c.connect(context.TODO(), "derphttp.Client.Recv")
|
||||
// WatchConnectionChanges sends a request to subscribe to
|
||||
// notifications about clients connecting & disconnecting.
|
||||
//
|
||||
// Only trusted connections (using MeshKey) are allowed to use this.
|
||||
func (c *Client) WatchConnectionChanges() error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.WatchConnectionChanges")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
m, err := client.Recv(b)
|
||||
err = client.WatchConnectionChanges()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ClosePeer")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.ClosePeer(target)
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Recv reads a message from c. The returned message may alias memory from Client.
|
||||
// The message should only be used until the next Client call.
|
||||
func (c *Client) Recv() (derp.ReceivedMessage, error) {
|
||||
m, _, err := c.RecvDetail()
|
||||
return m, err
|
||||
}
|
||||
|
||||
// RecvDetail is like Recv, but additional returns the connection generation on each message.
|
||||
// The connGen value is incremented every time the derphttp.Client reconnects to the server.
|
||||
func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
client, connGen, err := c.connect(context.TODO(), "derphttp.Client.Recv")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
@@ -313,3 +754,16 @@ func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
}
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.Public, 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")))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
}
|
||||
}
|
||||
return key.Public{}, 0
|
||||
}
|
||||
|
||||
@@ -5,33 +5,51 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
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" {
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Upgrade", "DERP")
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := h.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("Hijack failed: %v", err)
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := s.PublicKey()
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %x\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey[:])
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -19,22 +18,15 @@ import (
|
||||
)
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := key.NewPrivate()
|
||||
|
||||
const numClients = 3
|
||||
var serverPrivateKey key.Private
|
||||
if _, err := crand.Read(serverPrivateKey[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var clientPrivateKeys []key.Private
|
||||
for i := 0; i < numClients; i++ {
|
||||
var key key.Private
|
||||
if _, err := crand.Read(key[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPrivateKeys = append(clientPrivateKeys, key)
|
||||
}
|
||||
var clientKeys []key.Public
|
||||
for _, privKey := range clientPrivateKeys {
|
||||
clientKeys = append(clientKeys, privKey.Public())
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := key.NewPrivate()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
|
||||
s := derp.NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -81,6 +73,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if err := c.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("client %d Connect: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
|
||||
@@ -93,9 +86,13 @@ func TestSendRecv(t *testing.T) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
b := make([]byte, 1<<16)
|
||||
m, err := c.Recv(b)
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
t.Logf("client%d: %v", i, err)
|
||||
break
|
||||
}
|
||||
@@ -106,7 +103,7 @@ func TestSendRecv(t *testing.T) {
|
||||
case derp.PeerGoneMessage:
|
||||
// Ignore.
|
||||
case derp.ReceivedPacket:
|
||||
recvChs[i] <- m.Data
|
||||
recvChs[i] <- append([]byte(nil), m.Data...)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
@@ -119,7 +116,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -147,5 +144,13 @@ func TestSendRecv(t *testing.T) {
|
||||
recv(2, string(msg2))
|
||||
recvNothing(0)
|
||||
recvNothing(1)
|
||||
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
122
derp/derphttp/mesh_client.go
Normal file
122
derp/derphttp/mesh_client.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
|
||||
// connection changes.
|
||||
//
|
||||
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
|
||||
//
|
||||
// Otherwise, the add and remove funcs are called as clients come & go.
|
||||
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
|
||||
logf := c.logf
|
||||
const retryInterval = 5 * time.Second
|
||||
const statusInterval = 10 * time.Second
|
||||
var (
|
||||
mu sync.Mutex
|
||||
present = map[key.Public]bool{}
|
||||
loggedConnected = false
|
||||
)
|
||||
clear := func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(present) == 0 {
|
||||
return
|
||||
}
|
||||
logf("reconnected; clearing %d forwarding mappings", len(present))
|
||||
for k := range present {
|
||||
remove(k)
|
||||
}
|
||||
present = map[key.Public]bool{}
|
||||
}
|
||||
lastConnGen := 0
|
||||
lastStatus := time.Now()
|
||||
logConnectedLocked := func() {
|
||||
if loggedConnected {
|
||||
return
|
||||
}
|
||||
logf("connected; %d peers", len(present))
|
||||
loggedConnected = true
|
||||
}
|
||||
|
||||
const logConnectedDelay = 200 * time.Millisecond
|
||||
timer := time.AfterFunc(2*time.Second, func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
logConnectedLocked()
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
updatePeer := func(k key.Public, isPresent bool) {
|
||||
if isPresent {
|
||||
add(k)
|
||||
} else {
|
||||
remove(k)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if isPresent {
|
||||
present[k] = true
|
||||
if !loggedConnected {
|
||||
timer.Reset(logConnectedDelay)
|
||||
}
|
||||
} else {
|
||||
// If we got a peerGone message, that means the initial connection's
|
||||
// flood of peerPresent messages is done, so we can log already:
|
||||
logConnectedLocked()
|
||||
delete(present, k)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
err := c.WatchConnectionChanges()
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("WatchConnectionChanges: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.ServerPublicKey() == ignoreServerKey {
|
||||
logf("detected self-connect; ignoring host")
|
||||
return
|
||||
}
|
||||
for {
|
||||
m, connGen, err := c.RecvDetail()
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("Recv: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
break
|
||||
}
|
||||
if connGen != lastConnGen {
|
||||
lastConnGen = connGen
|
||||
clear()
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(key.Public(m), true)
|
||||
case derp.PeerGoneMessage:
|
||||
updatePeer(key.Public(m), false)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
|
||||
lastStatus = now
|
||||
logf("%d peers", len(present))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,155 +3,86 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package derpmap contains information about Tailscale.com's production DERP nodes.
|
||||
//
|
||||
// This package is only used by the "tailscale netcheck" command for debugging.
|
||||
// In normal operation the Tailscale nodes get this sent to them from the control
|
||||
// server.
|
||||
//
|
||||
// TODO: remove this package and make "tailscale netcheck" get the
|
||||
// list from the control server too.
|
||||
package derpmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// World is a set of DERP server.
|
||||
type World struct {
|
||||
servers []*Server
|
||||
ids []int
|
||||
byID map[int]*Server
|
||||
stun4 []string
|
||||
stun6 []string
|
||||
}
|
||||
|
||||
func (w *World) IDs() []int { return w.ids }
|
||||
func (w *World) STUN4() []string { return w.stun4 }
|
||||
func (w *World) STUN6() []string { return w.stun6 }
|
||||
func (w *World) ServerByID(id int) *Server { return w.byID[id] }
|
||||
|
||||
// LocationOfID returns the geographic name of a node, if present.
|
||||
func (w *World) LocationOfID(id int) string {
|
||||
if s, ok := w.byID[id]; ok {
|
||||
return s.Geo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *World) NodeIDOfSTUNServer(server string) int {
|
||||
// TODO: keep reverse map? Small enough to not matter for now.
|
||||
for _, s := range w.servers {
|
||||
if s.STUN4 == server || s.STUN6 == server {
|
||||
return s.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ForeachServer calls fn for each DERP server, in an unspecified order.
|
||||
func (w *World) ForeachServer(fn func(*Server)) {
|
||||
for _, s := range w.byID {
|
||||
fn(s)
|
||||
func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
|
||||
return &tailcfg.DERPNode{
|
||||
Name: suffix, // updated later
|
||||
RegionID: 0, // updated later
|
||||
IPv4: v4,
|
||||
IPv6: v6,
|
||||
}
|
||||
}
|
||||
|
||||
// Prod returns the production DERP nodes.
|
||||
func Prod() *World {
|
||||
return prod
|
||||
func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
region := &tailcfg.DERPRegion{
|
||||
RegionID: id,
|
||||
RegionName: name,
|
||||
RegionCode: code,
|
||||
Nodes: nodes,
|
||||
}
|
||||
for _, n := range nodes {
|
||||
n.Name = fmt.Sprintf("%d%s", id, n.Name)
|
||||
n.RegionID = id
|
||||
n.HostName = fmt.Sprintf("derp%s.tailscale.com", strings.TrimSuffix(n.Name, "a"))
|
||||
}
|
||||
return region
|
||||
}
|
||||
|
||||
func NewTestWorld(stun ...string) *World {
|
||||
w := &World{}
|
||||
for i, s := range stun {
|
||||
w.add(&Server{
|
||||
ID: i + 1,
|
||||
Geo: fmt.Sprintf("Testopolis-%d", i+1),
|
||||
STUN4: s,
|
||||
})
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func NewTestWorldWith(servers ...*Server) *World {
|
||||
w := &World{}
|
||||
for _, s := range servers {
|
||||
w.add(s)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
var prod = new(World) // ... a dazzling place I never knew
|
||||
|
||||
func addProd(id int, geo string) {
|
||||
prod.add(&Server{
|
||||
ID: id,
|
||||
Geo: geo,
|
||||
HostHTTPS: fmt.Sprintf("derp%v.tailscale.com", id),
|
||||
STUN4: fmt.Sprintf("derp%v.tailscale.com:3478", id),
|
||||
STUN6: fmt.Sprintf("derp%v-v6.tailscale.com:3478", id),
|
||||
})
|
||||
}
|
||||
|
||||
func (w *World) add(s *Server) {
|
||||
if s.ID == 0 {
|
||||
panic("ID required")
|
||||
}
|
||||
if _, dup := w.byID[s.ID]; dup {
|
||||
panic("duplicate prod server")
|
||||
}
|
||||
if w.byID == nil {
|
||||
w.byID = make(map[int]*Server)
|
||||
}
|
||||
w.byID[s.ID] = s
|
||||
w.ids = append(w.ids, s.ID)
|
||||
w.servers = append(w.servers, s)
|
||||
if s.STUN4 != "" {
|
||||
w.stun4 = append(w.stun4, s.STUN4)
|
||||
if _, _, err := net.SplitHostPort(s.STUN4); err != nil {
|
||||
panic("not a host:port: " + s.STUN4)
|
||||
}
|
||||
}
|
||||
if s.STUN6 != "" {
|
||||
w.stun6 = append(w.stun6, s.STUN6)
|
||||
if _, _, err := net.SplitHostPort(s.STUN6); err != nil {
|
||||
panic("not a host:port: " + s.STUN6)
|
||||
}
|
||||
// Prod returns Tailscale's map of relay servers.
|
||||
//
|
||||
// This list is only used by cmd/tailscale's netcheck subcommand. In
|
||||
// normal operation the Tailscale nodes get this sent to them from the
|
||||
// control server.
|
||||
//
|
||||
// This list is subject to change and should not be relied on.
|
||||
func Prod() *tailcfg.DERPMap {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: derpRegion(1, "nyc", "New York City",
|
||||
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
|
||||
),
|
||||
2: derpRegion(2, "sfo", "San Francisco",
|
||||
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
|
||||
),
|
||||
3: derpRegion(3, "sin", "Singapore",
|
||||
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
|
||||
),
|
||||
4: derpRegion(4, "fra", "Frankfurt",
|
||||
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
|
||||
),
|
||||
5: derpRegion(5, "syd", "Sydney",
|
||||
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
|
||||
),
|
||||
6: derpRegion(6, "blr", "Bangalore",
|
||||
derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"),
|
||||
),
|
||||
7: derpRegion(7, "tok", "Tokyo",
|
||||
derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"),
|
||||
),
|
||||
8: derpRegion(8, "lhr", "London",
|
||||
derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"),
|
||||
),
|
||||
9: derpRegion(9, "dfw", "Dallas",
|
||||
derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"),
|
||||
),
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
addProd(1, "New York")
|
||||
addProd(2, "San Francisco")
|
||||
addProd(3, "Singapore")
|
||||
addProd(4, "Frankfurt")
|
||||
addProd(5, "Sydney")
|
||||
}
|
||||
|
||||
// Server is configuration for a DERP server.
|
||||
type Server struct {
|
||||
_ structs.Incomparable
|
||||
|
||||
ID int
|
||||
|
||||
// HostHTTPS is the HTTPS hostname.
|
||||
HostHTTPS string
|
||||
|
||||
// STUN4 is the host:port of the IPv4 STUN server on this DERP
|
||||
// node. Required.
|
||||
STUN4 string
|
||||
|
||||
// STUN6 optionally provides the IPv6 host:port of the STUN
|
||||
// server on the DERP node.
|
||||
// It should be an IPv6-only address for now. (We currently make lazy
|
||||
// assumptions that the server names are unique.)
|
||||
STUN6 string
|
||||
|
||||
// Geo is a human-readable geographic region name of this server.
|
||||
Geo string
|
||||
}
|
||||
|
||||
func (s *Server) String() string {
|
||||
if s == nil {
|
||||
return "<nil *derpmap.Server>"
|
||||
}
|
||||
if s.Geo != "" {
|
||||
return fmt.Sprintf("%v (%v)", s.HostHTTPS, s.Geo)
|
||||
}
|
||||
return s.HostHTTPS
|
||||
}
|
||||
|
||||
179
disco/disco.go
Normal file
179
disco/disco.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package disco contains the discovery message types.
|
||||
//
|
||||
// A discovery message is:
|
||||
//
|
||||
// Header:
|
||||
// magic [6]byte // “TS💬” (0x54 53 f0 9f 92 ac)
|
||||
// senderDiscoPub [32]byte // nacl public key
|
||||
// nonce [24]byte
|
||||
//
|
||||
// The recipient then decrypts the bytes following (the nacl secretbox)
|
||||
// and then the inner payload structure is:
|
||||
//
|
||||
// messageType byte (the MessageType constants below)
|
||||
// messageVersion byte (0 for now; but always ignore bytes at the end)
|
||||
// message-paylod [...]byte
|
||||
package disco
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Magic is the 6 byte header of all discovery messages.
|
||||
const Magic = "TS💬" // 6 bytes: 0x54 53 f0 9f 92 ac
|
||||
|
||||
const keyLen = 32
|
||||
|
||||
// NonceLen is the length of the nonces used by nacl secretboxes.
|
||||
const NonceLen = 24
|
||||
|
||||
type MessageType byte
|
||||
|
||||
const (
|
||||
TypePing = MessageType(0x01)
|
||||
TypePong = MessageType(0x02)
|
||||
TypeCallMeMaybe = MessageType(0x03)
|
||||
)
|
||||
|
||||
const v0 = byte(0)
|
||||
|
||||
var errShort = errors.New("short message")
|
||||
|
||||
// LooksLikeDiscoWrapper reports whether p looks like it's a packet
|
||||
// containing an encrypted disco message.
|
||||
func LooksLikeDiscoWrapper(p []byte) bool {
|
||||
if len(p) < len(Magic)+keyLen+NonceLen {
|
||||
return false
|
||||
}
|
||||
return string(p[:len(Magic)]) == Magic
|
||||
}
|
||||
|
||||
// Parse parses the encrypted part of the message from inside the
|
||||
// nacl secretbox.
|
||||
func Parse(p []byte) (Message, error) {
|
||||
if len(p) < 2 {
|
||||
return nil, errShort
|
||||
}
|
||||
t, ver, p := MessageType(p[0]), p[1], p[2:]
|
||||
switch t {
|
||||
case TypePing:
|
||||
return parsePing(ver, p)
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return CallMeMaybe{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
}
|
||||
|
||||
// Message a discovery message.
|
||||
type Message interface {
|
||||
// AppendMarshal appends the message's marshaled representation.
|
||||
AppendMarshal([]byte) []byte
|
||||
}
|
||||
|
||||
// appendMsgHeader appends two bytes (for t and ver) and then also
|
||||
// dataLen bytes to b, returning the appended slice in all. The
|
||||
// returned data slice is a subslice of all with just dataLen bytes of
|
||||
// where the caller will fill in the data.
|
||||
func appendMsgHeader(b []byte, t MessageType, ver uint8, dataLen int) (all, data []byte) {
|
||||
// TODO: optimize this?
|
||||
all = append(b, make([]byte, dataLen+2)...)
|
||||
all[len(b)] = byte(t)
|
||||
all[len(b)+1] = ver
|
||||
data = all[len(b)+2:]
|
||||
return
|
||||
}
|
||||
|
||||
type Ping struct {
|
||||
TxID [12]byte
|
||||
}
|
||||
|
||||
func (m *Ping) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePing, v0, 12)
|
||||
copy(d, m.TxID[:])
|
||||
return ret
|
||||
}
|
||||
|
||||
func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
if len(p) < 12 {
|
||||
return nil, errShort
|
||||
}
|
||||
m = new(Ping)
|
||||
copy(m.TxID[:], p)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CallMeMaybe is a message sent only over DERP to request that the recipient try
|
||||
// to open up a magicsock path back to the sender.
|
||||
//
|
||||
// The sender should've already sent UDP packets to the peer to open
|
||||
// up the stateful firewall mappings inbound.
|
||||
//
|
||||
// The recipient may choose to not open a path back, if it's already
|
||||
// happy with its path. But usually it will.
|
||||
type CallMeMaybe struct{}
|
||||
|
||||
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
|
||||
return ret
|
||||
}
|
||||
|
||||
// Pong is a response a Ping.
|
||||
//
|
||||
// It includes the sender's source IP + port, so it's effectively a
|
||||
// STUN response.
|
||||
type Pong struct {
|
||||
TxID [12]byte
|
||||
Src netaddr.IPPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4
|
||||
}
|
||||
|
||||
const pongLen = 12 + 16 + 2
|
||||
|
||||
func (m *Pong) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
|
||||
d = d[copy(d, m.TxID[:]):]
|
||||
ip16 := m.Src.IP.As16()
|
||||
d = d[copy(d, ip16[:]):]
|
||||
binary.BigEndian.PutUint16(d, m.Src.Port)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parsePong(ver uint8, p []byte) (m *Pong, err error) {
|
||||
if len(p) < pongLen {
|
||||
return nil, errShort
|
||||
}
|
||||
m = new(Pong)
|
||||
copy(m.TxID[:], p)
|
||||
p = p[12:]
|
||||
|
||||
m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16]))
|
||||
p = p[16:]
|
||||
|
||||
m.Src.Port = binary.BigEndian.Uint16(p)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// MessageSummary returns a short summary of m for logging purposes.
|
||||
func MessageSummary(m Message) string {
|
||||
switch m := m.(type) {
|
||||
case *Ping:
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
}
|
||||
}
|
||||
82
disco/disco_test.go
Normal file
82
disco/disco_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package disco
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestMarshalAndParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
m Message
|
||||
}{
|
||||
{
|
||||
name: "ping",
|
||||
m: &Ping{
|
||||
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
},
|
||||
want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c",
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
m: &Pong{
|
||||
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
Src: mustIPPort("2.3.4.5:1234"),
|
||||
},
|
||||
want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 00 00 00 00 00 00 00 00 00 ff ff 02 03 04 05 04 d2",
|
||||
},
|
||||
{
|
||||
name: "pongv6",
|
||||
m: &Pong{
|
||||
TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
Src: mustIPPort("[fed0::12]:6666"),
|
||||
},
|
||||
want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c fe d0 00 00 00 00 00 00 00 00 00 00 00 00 00 12 1a 0a",
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe",
|
||||
m: CallMeMaybe{},
|
||||
want: "03 00",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
foo := []byte("foo")
|
||||
got := string(tt.m.AppendMarshal(foo))
|
||||
if !strings.HasPrefix(got, "foo") {
|
||||
t.Fatalf("didn't start with foo: got %q", got)
|
||||
}
|
||||
got = strings.TrimPrefix(got, "foo")
|
||||
|
||||
gotHex := fmt.Sprintf("% x", got)
|
||||
if gotHex != tt.want {
|
||||
t.Fatalf("wrong marshal\n got: %s\nwant: %s\n", gotHex, tt.want)
|
||||
}
|
||||
|
||||
back, err := Parse([]byte(got))
|
||||
if err != nil {
|
||||
t.Fatalf("parse back: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(back, tt.m) {
|
||||
t.Errorf("message in %+v doesn't match Parse back result %+v", tt.m, back)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustIPPort(s string) netaddr.IPPort {
|
||||
ipp, err := netaddr.ParseIPPort(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipp
|
||||
}
|
||||
38
go.mod
38
go.mod
@@ -1,33 +1,45 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/klauspost/compress v1.9.8
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mdlayher/netlink v1.1.0
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200424121617-8d10f231531a
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20200411205429-f77f31c81751
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb // indirect
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
gortc.io/stun v1.22.1
|
||||
inet.af/netaddr v0.0.0-20200417213433-f9e5bcc2d6ea
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
185
go.sum
185
go.sum
@@ -3,12 +3,11 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
|
||||
@@ -24,12 +23,17 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
@@ -38,6 +42,12 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
@@ -47,20 +57,35 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA=
|
||||
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
|
||||
@@ -68,83 +93,175 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f h1:uFj5bslHsMzxIM8UTjAhq4VXeo6GfNW91rpoh/WMJaY=
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200424121617-8d10f231531a h1:HMkTFyhcvZaKf7+7T76rks4HqB83fptUemBIfLGI6TM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200424121617-8d10f231531a/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf h1:HuBwLWbDNIh/G72KSImSEx+dnd7FPGFI1e60LMJtLjU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf/go.mod h1:9PbAnF5CAklkURoO0uQhm+YUjDmm9T9oCyTGlCHuTPQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d h1:ha3qx0YBsEYM1VpLoAxVyLsz74H2a/Kv/id+2Bo/WLU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d/go.mod h1:FEGDKc5yHNWtTS5ugWnHMNF0d9LlaHv/zQwOrVogo2U=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a h1:RUJeuZlAm1DT6Mhk9UTsaHrDeDZhPrbKfNsaEtKF6+0=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a/go.mod h1:UIAx57STfAZOrNVj8QGP2zG3ovWPMTD4DDubFHqMlYI=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
go4.org/mem v0.0.0-20200411205429-f77f31c81751 h1:sgGPu7KkyLjyOYOwKFHCtnfosdSuM5q2Gud23Y/+nzw=
|
||||
go4.org/mem v0.0.0-20200411205429-f77f31c81751/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww=
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44=
|
||||
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gortc.io/stun v1.22.1 h1:96mOdDATYRqhYB+TZdenWBg4CzL2Ye5kPyBXQ8KAB+8=
|
||||
gortc.io/stun v1.22.1/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg=
|
||||
inet.af/netaddr v0.0.0-20200417213433-f9e5bcc2d6ea h1:DpXewrGVf9+vvYQFrNGj9v34bXMuTVQv+2wuULTNV8I=
|
||||
inet.af/netaddr v0.0.0-20200417213433-f9e5bcc2d6ea/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201218162718-658fec415e52/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac h1:aqMW8vft7VmOIhtQhsTWhAuZzOBGYBv+Otyvwj+VGSU=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437 h1:Li2QBwaT/hU3wE7GdyoqaX+TzIlI+V1zs/CuWrjX8e4=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610 h1:9Nnw3NS9SL4SlFtBWSdv7onMbdY+B8nflRNZvhgxuMY=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016 h1:CEeeAJW60aRKE6gGJC5krs2xC/uM2l8SasvgeDXFN5Q=
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016/go.mod h1:lbePDLSB5c45kkUmF7ETNE5X9z/yuQvWJIv1hhb5rFI=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
tailscale.com v1.2.10/go.mod h1:JEJiCce3MHtPCTdX2ahLc4tcnxZ7b5etish1Yt0B6+w=
|
||||
|
||||
103
internal/deepprint/deepprint.go
Normal file
103
internal/deepprint/deepprint.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package deepprint walks a Go value recursively, in a predictable
|
||||
// order, without looping, and prints each value out to a given
|
||||
// Writer, which is assumed to be a hash.Hash, as this package doesn't
|
||||
// format things nicely.
|
||||
//
|
||||
// This is intended as a lighter version of go-spew, etc. We don't need its
|
||||
// features when our writer is just a hash.
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
Print(h, v)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
fmt.Fprintf(w, "struct{\n")
|
||||
t := v.Type()
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
fmt.Fprintf(w, "%s: ", sf.Name)
|
||||
print(w, v.Field(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
fmt.Fprintf(w, ": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, "%s", v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
71
internal/deepprint/deepprint_test.go
Normal file
71
internal/deepprint/deepprint_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
// v contains the types of values we care about for our current callers.
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
var buf bytes.Buffer
|
||||
Print(&buf, v)
|
||||
t.Logf("Got: %s", buf.Bytes())
|
||||
|
||||
hash1 := Hash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
hash2 := Hash(getVal())
|
||||
if hash1 != hash2 {
|
||||
t.Error("second hash didn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: []wgcfg.Endpoint{
|
||||
{
|
||||
Host: "foo",
|
||||
Port: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
DNS: dns.Config{
|
||||
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
|
||||
Domains: []string{"tailscale.net"},
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
"key3": "val3",
|
||||
"key4": "val4",
|
||||
"key5": "val5",
|
||||
"key6": "val6",
|
||||
"key7": "val7",
|
||||
"key8": "val8",
|
||||
"key9": "val9",
|
||||
},
|
||||
}
|
||||
}
|
||||
224
internal/deepprint/fmtsort.go
Normal file
224
internal/deepprint/fmtsort.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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.
|
||||
|
||||
// and
|
||||
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This is a slightly modified fork of Go's src/internal/fmtsort/sort.go
|
||||
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Note: Throughout this package we avoid calling reflect.Value.Interface as
|
||||
// it is not always legal to do so and it's easier to avoid the issue than to face it.
|
||||
|
||||
// sortedMap represents a map's keys and values. The keys and values are
|
||||
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
|
||||
type sortedMap struct {
|
||||
Key []reflect.Value
|
||||
Value []reflect.Value
|
||||
}
|
||||
|
||||
func (o *sortedMap) Len() int { return len(o.Key) }
|
||||
func (o *sortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
|
||||
func (o *sortedMap) Swap(i, j int) {
|
||||
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
|
||||
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
|
||||
}
|
||||
|
||||
// Sort accepts a map and returns a sortedMap that has the same keys and
|
||||
// values but in a stable sorted order according to the keys, modulo issues
|
||||
// raised by unorderable key values such as NaNs.
|
||||
//
|
||||
// The ordering rules are more general than with Go's < operator:
|
||||
//
|
||||
// - when applicable, nil compares low
|
||||
// - ints, floats, and strings order by <
|
||||
// - NaN compares less than non-NaN floats
|
||||
// - bool compares false before true
|
||||
// - complex compares real, then imag
|
||||
// - pointers compare by machine address
|
||||
// - channel values compare by machine address
|
||||
// - structs compare each field in turn
|
||||
// - arrays compare each element in turn.
|
||||
// Otherwise identical arrays compare by length.
|
||||
// - interface values compare first by reflect.Type describing the concrete type
|
||||
// and then by concrete value as described in the previous rules.
|
||||
//
|
||||
func newSortedMap(mapValue reflect.Value) *sortedMap {
|
||||
if mapValue.Type().Kind() != reflect.Map {
|
||||
return nil
|
||||
}
|
||||
// Note: this code is arranged to not panic even in the presence
|
||||
// of a concurrent map update. The runtime is responsible for
|
||||
// yelling loudly if that happens. See issue 33275.
|
||||
n := mapValue.Len()
|
||||
key := make([]reflect.Value, 0, n)
|
||||
value := make([]reflect.Value, 0, n)
|
||||
iter := mapValue.MapRange()
|
||||
for iter.Next() {
|
||||
key = append(key, iter.Key())
|
||||
value = append(value, iter.Value())
|
||||
}
|
||||
sorted := &sortedMap{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
sort.Stable(sorted)
|
||||
return sorted
|
||||
}
|
||||
|
||||
// compare compares two values of the same type. It returns -1, 0, 1
|
||||
// according to whether a > b (1), a == b (0), or a < b (-1).
|
||||
// If the types differ, it returns -1.
|
||||
// See the comment on Sort for the comparison rules.
|
||||
func compare(aVal, bVal reflect.Value) int {
|
||||
aType, bType := aVal.Type(), bVal.Type()
|
||||
if aType != bType {
|
||||
return -1 // No good answer possible, but don't return 0: they're not equal.
|
||||
}
|
||||
switch aVal.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
a, b := aVal.Int(), bVal.Int()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
a, b := aVal.Uint(), bVal.Uint()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.String:
|
||||
a, b := aVal.String(), bVal.String()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatCompare(aVal.Float(), bVal.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
a, b := aVal.Complex(), bVal.Complex()
|
||||
if c := floatCompare(real(a), real(b)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return floatCompare(imag(a), imag(b))
|
||||
case reflect.Bool:
|
||||
a, b := aVal.Bool(), bVal.Bool()
|
||||
switch {
|
||||
case a == b:
|
||||
return 0
|
||||
case a:
|
||||
return 1
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
case reflect.Ptr:
|
||||
a, b := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Chan:
|
||||
if c, ok := nilCompare(aVal, bVal); ok {
|
||||
return c
|
||||
}
|
||||
ap, bp := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case ap < bp:
|
||||
return -1
|
||||
case ap > bp:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < aVal.NumField(); i++ {
|
||||
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case reflect.Array:
|
||||
for i := 0; i < aVal.Len(); i++ {
|
||||
if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case reflect.Interface:
|
||||
if c, ok := nilCompare(aVal, bVal); ok {
|
||||
return c
|
||||
}
|
||||
c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type()))
|
||||
if c != 0 {
|
||||
return c
|
||||
}
|
||||
return compare(aVal.Elem(), bVal.Elem())
|
||||
default:
|
||||
// Certain types cannot appear as keys (maps, funcs, slices), but be explicit.
|
||||
panic("bad type in compare: " + aType.String())
|
||||
}
|
||||
}
|
||||
|
||||
// nilCompare checks whether either value is nil. If not, the boolean is false.
|
||||
// If either value is nil, the boolean is true and the integer is the comparison
|
||||
// value. The comparison is defined to be 0 if both are nil, otherwise the one
|
||||
// nil value compares low. Both arguments must represent a chan, func,
|
||||
// interface, map, pointer, or slice.
|
||||
func nilCompare(aVal, bVal reflect.Value) (int, bool) {
|
||||
if aVal.IsNil() {
|
||||
if bVal.IsNil() {
|
||||
return 0, true
|
||||
}
|
||||
return -1, true
|
||||
}
|
||||
if bVal.IsNil() {
|
||||
return 1, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// floatCompare compares two floating-point values. NaNs compare low.
|
||||
func floatCompare(a, b float64) int {
|
||||
switch {
|
||||
case isNaN(a):
|
||||
return -1 // No good answer if b is a NaN so don't bother checking.
|
||||
case isNaN(b):
|
||||
return 1
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func isNaN(a float64) bool {
|
||||
return a != a
|
||||
}
|
||||
9
internal/tooldeps/tooldeps.go
Normal file
9
internal/tooldeps/tooldeps.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tooldeps
|
||||
|
||||
import (
|
||||
_ "github.com/tailscale/depaware/depaware"
|
||||
)
|
||||
@@ -5,8 +5,10 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -19,6 +21,7 @@ type State int
|
||||
|
||||
const (
|
||||
NoState = State(iota)
|
||||
InUseOtherUser
|
||||
NeedsLogin
|
||||
NeedsMachineAuth
|
||||
Stopped
|
||||
@@ -26,9 +29,19 @@ const (
|
||||
Running
|
||||
)
|
||||
|
||||
// GoogleIDToken Type is the oauth2.Token.TokenType for the Google
|
||||
// ID tokens used by the Android client.
|
||||
const GoogleIDTokenType = "ts_android_google_login"
|
||||
|
||||
func (s State) String() string {
|
||||
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
|
||||
"Stopped", "Starting", "Running"}[s]
|
||||
return [...]string{
|
||||
"NoState",
|
||||
"InUseOtherUser",
|
||||
"NeedsLogin",
|
||||
"NeedsMachineAuth",
|
||||
"Stopped",
|
||||
"Starting",
|
||||
"Running"}[s]
|
||||
}
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
@@ -39,8 +52,6 @@ type EngineStatus struct {
|
||||
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
|
||||
}
|
||||
|
||||
type NetworkMap = controlclient.NetworkMap
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
// (cmd/tailscale, iOS, macOS, Win Tasktray).
|
||||
// In any given notification, any or all of these may be nil, meaning
|
||||
@@ -48,16 +59,23 @@ type NetworkMap = controlclient.NetworkMap
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *controlclient.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
// This is currently only used by Tailscale when run in the
|
||||
// macOS Network Extension.
|
||||
LocalTCPPort *uint16 `json:",omitempty"`
|
||||
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
@@ -70,14 +88,14 @@ type Notify struct {
|
||||
// shared by several consecutive users. Ideally we would just use the
|
||||
// username of the connected frontend as the StateKey.
|
||||
//
|
||||
// However, on Windows, there seems to be no safe way to figure out
|
||||
// the owning user of a process connected over IPC mechanisms
|
||||
// (sockets, named pipes). So instead, on Windows, we use a
|
||||
// capability-oriented system where the frontend generates a random
|
||||
// identifier for itself, and uses that as the StateKey when talking
|
||||
// to the backend. That way, while we can't identify an OS user by
|
||||
// name, we can tell two different users apart, because they'll have
|
||||
// different opaque state keys (and no access to each others's keys).
|
||||
// Various platforms currently set StateKey in different ways:
|
||||
//
|
||||
// * the macOS/iOS GUI apps set it to "ipn-go-bridge"
|
||||
// * the Android app sets it to "ipn-android"
|
||||
// * on Windows, it's the empty string (in client mode) or, via
|
||||
// LocalBackend.userID, a string like "user-$USER_ID" (used in
|
||||
// server mode).
|
||||
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
|
||||
type StateKey string
|
||||
|
||||
type Options struct {
|
||||
@@ -86,16 +104,17 @@ type Options struct {
|
||||
// StateKey and Prefs together define the state the backend should
|
||||
// use:
|
||||
// - StateKey=="" && Prefs!=nil: use Prefs for internal state,
|
||||
// don't persist changes in the backend.
|
||||
// don't persist changes in the backend, except for the machine key
|
||||
// for migration purposes.
|
||||
// - StateKey!="" && Prefs==nil: load the given backend-side
|
||||
// state and use/update that.
|
||||
// - StateKey!="" && Prefs!=nil: like the previous case, but do
|
||||
// an initial overwrite of backend state with Prefs.
|
||||
StateKey StateKey
|
||||
Prefs *Prefs
|
||||
// AuthKey is an optional node auth key used to authorize a
|
||||
// new node key without user interaction.
|
||||
AuthKey string
|
||||
Prefs *Prefs
|
||||
// LegacyConfigPath optionally specifies the old-style relaynode
|
||||
// relay.conf location. If both LegacyConfigPath and StateKey are
|
||||
// specified and the requested state doesn't exist in the backend
|
||||
@@ -106,6 +125,9 @@ type Options struct {
|
||||
LegacyConfigPath string
|
||||
// Notify is called when backend events happen.
|
||||
Notify func(Notify) `json:"-"`
|
||||
// HTTPTestClient is an optional HTTP client to pass to controlclient
|
||||
// (for tests only).
|
||||
HTTPTestClient *http.Client
|
||||
}
|
||||
|
||||
// Backend is the interface between Tailscale frontends
|
||||
@@ -121,6 +143,8 @@ type Backend interface {
|
||||
// flow. This should trigger a new BrowseToURL notification
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *oauth2.Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
@@ -128,6 +152,9 @@ type Backend interface {
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*Prefs)
|
||||
// SetWantRunning is like SetPrefs but sets only the
|
||||
// WantRunning field.
|
||||
SetWantRunning(wantRunning bool)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
@@ -141,4 +168,8 @@ type Backend interface {
|
||||
// make sure they react properly with keys that are going to
|
||||
// expire.
|
||||
FakeExpireAfter(x time.Duration)
|
||||
// Ping attempts to start connecting to the given IP and sends a Notify
|
||||
// with its PingResult. If the host is down, there might never
|
||||
// be a PingResult sent. The cmd/tailscale CLI client adds a timeout.
|
||||
Ping(ip string)
|
||||
}
|
||||
|
||||
279
ipn/e2e_test.go
279
ipn/e2e_test.go
@@ -1,279 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build depends_on_currently_unreleased
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.io/control" // not yet released
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Hacky way to signal to magicsock for now not to bind on the
|
||||
// unspecified address. TODO(bradfitz): clean up wgengine's
|
||||
// constructors.
|
||||
os.Setenv("IN_TS_TEST", "1")
|
||||
}
|
||||
|
||||
func TestIPN(t *testing.T) {
|
||||
tstest.FixLogs(t)
|
||||
defer tstest.UnfixLogs(t)
|
||||
|
||||
// Turn off STUN for the test to make it hermetic.
|
||||
// TODO(crawshaw): add a test that runs against a local STUN server.
|
||||
magicsock.DisableSTUNForTesting = true
|
||||
defer func() { magicsock.DisableSTUNForTesting = false }()
|
||||
|
||||
// TODO(apenwarr): Make resource checks actually pass.
|
||||
// They don't right now, because (at least) wgengine doesn't fully
|
||||
// shut down.
|
||||
// rc := tstest.NewResourceCheck()
|
||||
// defer rc.Assert(t)
|
||||
|
||||
var ctl *control.Server
|
||||
|
||||
ctlHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctl.ServeHTTP(w, r)
|
||||
}
|
||||
https := httptest.NewServer(http.HandlerFunc(ctlHandler))
|
||||
serverURL := https.URL
|
||||
defer https.Close()
|
||||
defer https.CloseClientConnections()
|
||||
|
||||
tmpdir, err := ioutil.TempDir("", "ipntest")
|
||||
if err != nil {
|
||||
t.Fatalf("create tempdir: %v\n", err)
|
||||
}
|
||||
ctl, err = control.New(tmpdir, tmpdir, tmpdir, serverURL, true)
|
||||
if err != nil {
|
||||
t.Fatalf("create control server: %v\n", ctl)
|
||||
}
|
||||
if _, err := ctl.DB().FindOrCreateUser("google", "test1@example.com", "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n1 := newNode(t, "n1", https, false)
|
||||
defer n1.Backend.Shutdown()
|
||||
n1.Backend.StartLoginInteractive()
|
||||
|
||||
n2 := newNode(t, "n2", https, true)
|
||||
defer n2.Backend.Shutdown()
|
||||
n2.Backend.StartLoginInteractive()
|
||||
|
||||
t.Run("login", func(t *testing.T) {
|
||||
var s1, s2 State
|
||||
for {
|
||||
t.Logf("\n\nn1.state=%v n2.state=%v\n\n", s1, s2)
|
||||
|
||||
// TODO(crawshaw): switch from || to &&. To do this we need to
|
||||
// transmit some data so that the handshake completes on both
|
||||
// sides. (Because handshakes are 1RTT, it is the data
|
||||
// transmission that completes the handshake.)
|
||||
if s1 == Running || s2 == Running {
|
||||
// TODO(apenwarr): ensure state sequence.
|
||||
// Right now we'll just exit as soon as
|
||||
// state==Running, even if the backend is lying or
|
||||
// something. Not a great test.
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case n := <-n1.NotifyCh:
|
||||
t.Logf("n1n: %v\n", n)
|
||||
if n.State != nil {
|
||||
s1 = *n.State
|
||||
if s1 == NeedsMachineAuth {
|
||||
authNode(t, ctl, n1.Backend)
|
||||
}
|
||||
}
|
||||
case n := <-n2.NotifyCh:
|
||||
t.Logf("n2n: %v\n", n)
|
||||
if n.State != nil {
|
||||
s2 = *n.State
|
||||
if s2 == NeedsMachineAuth {
|
||||
authNode(t, ctl, n2.Backend)
|
||||
}
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("\n\n\nFATAL: timed out waiting for notifications.\n\n\n")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
n1addr := n1.Backend.NetMap().Addresses[0].IP
|
||||
n2addr := n2.Backend.NetMap().Addresses[0].IP
|
||||
t.Run("ping n2", func(t *testing.T) {
|
||||
t.Skip("TODO(crawshaw): skipping ping test, it is flaky")
|
||||
msg := tuntest.Ping(n2addr.IP(), n1addr.IP())
|
||||
n1.ChannelTUN.Outbound <- msg
|
||||
select {
|
||||
case msgRecv := <-n2.ChannelTUN.Inbound:
|
||||
if !bytes.Equal(msg, msgRecv) {
|
||||
t.Error("bad ping")
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("no ping seen")
|
||||
}
|
||||
})
|
||||
t.Run("ping n1", func(t *testing.T) {
|
||||
t.Skip("TODO(crawshaw): skipping ping test, it is flaky")
|
||||
msg := tuntest.Ping(n1addr.IP(), n2addr.IP())
|
||||
n2.ChannelTUN.Outbound <- msg
|
||||
select {
|
||||
case msgRecv := <-n1.ChannelTUN.Inbound:
|
||||
if !bytes.Equal(msg, msgRecv) {
|
||||
t.Error("bad ping")
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("no ping seen")
|
||||
}
|
||||
})
|
||||
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case <-n1.NotifyCh:
|
||||
case <-n2.NotifyCh:
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
|
||||
n1.Backend.Logout()
|
||||
|
||||
t.Run("logout", func(t *testing.T) {
|
||||
var s State
|
||||
for {
|
||||
select {
|
||||
case n := <-n1.NotifyCh:
|
||||
if n.State == nil {
|
||||
continue
|
||||
}
|
||||
s = *n.State
|
||||
t.Logf("n.State=%v", s)
|
||||
if s == NeedsLogin {
|
||||
return
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("timeout waiting for logout State=NeedsLogin, got State=%v", s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type testNode struct {
|
||||
Backend *LocalBackend
|
||||
ChannelTUN *tuntest.ChannelTUN
|
||||
NotifyCh <-chan Notify
|
||||
}
|
||||
|
||||
// Create a new IPN node.
|
||||
func newNode(t *testing.T, prefix string, https *httptest.Server, weirdPrefs bool) testNode {
|
||||
t.Helper()
|
||||
logfe := func(fmt string, args ...interface{}) {
|
||||
t.Logf(prefix+".e: "+fmt, args...)
|
||||
}
|
||||
logf := func(fmt string, args ...interface{}) {
|
||||
t.Logf(prefix+": "+fmt, args...)
|
||||
}
|
||||
|
||||
var err error
|
||||
httpc := https.Client()
|
||||
httpc.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
e1, err := wgengine.NewUserspaceEngineAdvanced(logfe, tun.TUN(), router.NewFake, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeEngine: %v\n", err)
|
||||
}
|
||||
n, err := NewLocalBackend(logf, prefix, &MemoryStore{}, e1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v\n", err)
|
||||
}
|
||||
nch := make(chan Notify, 1000)
|
||||
c := controlclient.Persist{
|
||||
Provider: "google",
|
||||
LoginName: "test1@example.com",
|
||||
}
|
||||
prefs := NewPrefs()
|
||||
prefs.ControlURL = https.URL
|
||||
prefs.Persist = &c
|
||||
|
||||
if weirdPrefs {
|
||||
// Let's test some nonempty extra prefs fields to make sure
|
||||
// the server can handle them.
|
||||
prefs.AdvertiseTags = []string{"tag:abc"}
|
||||
cidr, err := wgcfg.ParseCIDR("1.2.3.4/24")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCIDR: %v", err)
|
||||
}
|
||||
prefs.AdvertiseRoutes = []wgcfg.CIDR{cidr}
|
||||
}
|
||||
|
||||
n.Start(Options{
|
||||
FrontendLogID: prefix + "-f",
|
||||
Prefs: prefs,
|
||||
Notify: func(n Notify) {
|
||||
// Automatically visit auth URLs
|
||||
if n.BrowseToURL != nil {
|
||||
t.Logf("BrowseToURL: %v", *n.BrowseToURL)
|
||||
|
||||
authURL := *n.BrowseToURL
|
||||
i := strings.Index(authURL, "/a/")
|
||||
if i == -1 {
|
||||
panic("bad authURL: " + authURL)
|
||||
}
|
||||
authURL = authURL[:i] + "/login?refresh=true&next_url=" + url.PathEscape(authURL[i:])
|
||||
|
||||
form := url.Values{"user": []string{c.LoginName}}
|
||||
req, err := http.NewRequest("POST", authURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
if _, err := httpc.Do(req); err != nil {
|
||||
t.Logf("BrowseToURL: %v\n", err)
|
||||
}
|
||||
}
|
||||
nch <- n
|
||||
},
|
||||
})
|
||||
|
||||
return testNode{
|
||||
Backend: n,
|
||||
ChannelTUN: tun,
|
||||
NotifyCh: nch,
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the control server to authorize the given node.
|
||||
func authNode(t *testing.T, ctl *control.Server, n *LocalBackend) {
|
||||
mk := n.prefs.Persist.PrivateMachineKey.Public()
|
||||
nk := n.prefs.Persist.PrivateNodeKey.Public()
|
||||
ctl.AuthorizeMachine(tailcfg.MachineKey(mk), tailcfg.NodeKey(nk))
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
@@ -41,10 +43,18 @@ func (b *FakeBackend) newState(s State) {
|
||||
func (b *FakeBackend) StartLoginInteractive() {
|
||||
u := b.serverURL + "/this/is/fake"
|
||||
b.notify(Notify{BrowseToURL: &u})
|
||||
b.login()
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Login(token *oauth2.Token) {
|
||||
b.login()
|
||||
}
|
||||
|
||||
func (b *FakeBackend) login() {
|
||||
b.newState(NeedsMachineAuth)
|
||||
b.newState(Stopped)
|
||||
// TODO(apenwarr): Fill in a more interesting netmap here.
|
||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.newState(Starting)
|
||||
// TODO(apenwarr): Fill in a more interesting status.
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
@@ -69,6 +79,10 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetWantRunning(v bool) {
|
||||
b.SetPrefs(&Prefs{WantRunning: v})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
@@ -78,5 +92,9 @@ func (b *FakeBackend) RequestStatus() {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -20,7 +22,7 @@ type Handle struct {
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
netmapCache *NetworkMap
|
||||
netmapCache *controlclient.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
@@ -116,7 +118,7 @@ func (h *Handle) EngineStatus() EngineStatus {
|
||||
return h.engineStatusCache
|
||||
}
|
||||
|
||||
func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -124,10 +126,10 @@ func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []wgcfg.CIDR{}
|
||||
return []netaddr.IPPrefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *NetworkMap {
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -153,6 +155,10 @@ func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Login(token *oauth2.Token) {
|
||||
h.b.Login(token)
|
||||
}
|
||||
|
||||
func (h *Handle) Logout() {
|
||||
h.b.Logout()
|
||||
}
|
||||
|
||||
@@ -7,23 +7,34 @@ package ipnserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -33,15 +44,19 @@ type Options struct {
|
||||
// SocketPath, on unix systems, is the unix socket path to listen
|
||||
// on for frontend connections.
|
||||
SocketPath string
|
||||
|
||||
// Port, on windows, is the localhost TCP port to listen on for
|
||||
// frontend connections.
|
||||
Port int
|
||||
|
||||
// StatePath is the path to the stored agent state.
|
||||
StatePath string
|
||||
|
||||
// AutostartStateKey, if non-empty, immediately starts the agent
|
||||
// using the given StateKey. If empty, the agent stays idle and
|
||||
// waits for a frontend to start it.
|
||||
AutostartStateKey ipn.StateKey
|
||||
|
||||
// LegacyConfigPath optionally specifies the old-style relaynode
|
||||
// relay.conf location. If both LegacyConfigPath and
|
||||
// AutostartStateKey are specified and the requested state doesn't
|
||||
@@ -51,10 +66,21 @@ type Options struct {
|
||||
// TODO(danderson): remove some time after the transition to
|
||||
// tailscaled is done.
|
||||
LegacyConfigPath string
|
||||
|
||||
// SurviveDisconnects specifies how the server reacts to its
|
||||
// frontend disconnecting. If true, the server keeps running on
|
||||
// its existing state, and accepts new frontend connections. If
|
||||
// false, the server dumps its state and becomes idle.
|
||||
//
|
||||
// This is effectively whether the platform is in "server
|
||||
// mode" by default. On Linux, it's true; on Windows, it's
|
||||
// false. But on some platforms (currently only Windows), the
|
||||
// "server mode" can be overridden at runtime with a change in
|
||||
// Prefs.ForceDaemon/WantRunning.
|
||||
//
|
||||
// To support CLI connections (notably, "tailscale status"),
|
||||
// the actual definition of "disconnect" is when the
|
||||
// connection count transitions from 1 to 0.
|
||||
SurviveDisconnects bool
|
||||
|
||||
// DebugMux, if non-nil, specifies an HTTP ServeMux in which
|
||||
@@ -62,81 +88,501 @@ type Options struct {
|
||||
DebugMux *http.ServeMux
|
||||
}
|
||||
|
||||
func pump(logf logger.Logf, ctx context.Context, bs *ipn.BackendServer, s net.Conn) {
|
||||
defer logf("Control connection done.")
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
b *ipn.LocalBackend
|
||||
logf logger.Logf
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 connections. That is, this is whether the backend is
|
||||
// being run in "client mode" that requires an active GUI
|
||||
// connection (such as on Windows by default). Even if this
|
||||
// is true, the ForceDaemon pref can override this.
|
||||
resetOnZero bool
|
||||
|
||||
for ctx.Err() == nil && !bs.GotQuit {
|
||||
msg, err := ipn.ReadMsg(s)
|
||||
if err != nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
break
|
||||
bsMu sync.Mutex // lock order: bsMu, then mu
|
||||
bs *ipn.BackendServer
|
||||
|
||||
mu sync.Mutex
|
||||
serverModeUser *user.User // or nil if not in server mode
|
||||
lastUserID string // tracks last userid; on change, Reset state for paranoia
|
||||
allClients map[net.Conn]connIdentity // HTTP or IPN
|
||||
clients map[net.Conn]bool // subset of allClients; only IPN protocol
|
||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||
}
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||
// and a ConnIdentity with Unknown set true. It's only an error if we expected
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
return connIdentity{Unknown: true}, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("parsing local address: %w", err)
|
||||
}
|
||||
ra, err := netaddr.ParseIPPort(c.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("parsing local remote: %w", err)
|
||||
}
|
||||
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
|
||||
return ci, errors.New("non-loopback connection")
|
||||
}
|
||||
tab, err := netstat.Get()
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to get local connection table: %w", err)
|
||||
}
|
||||
pid := peerPid(tab.Entries, la, ra)
|
||||
if pid == 0 {
|
||||
return ci, errors.New("no local process found matching localhost connection")
|
||||
}
|
||||
ci.Pid = pid
|
||||
uid, err := pidowner.OwnerOfPID(pid)
|
||||
if err != nil {
|
||||
var hint string
|
||||
if runtime.GOOS == "windows" {
|
||||
hint = " (WSL?)"
|
||||
}
|
||||
err = bs.GotCommandMsg(msg)
|
||||
if err != nil {
|
||||
logf("GotCommandMsg: %v", err)
|
||||
break
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
|
||||
}
|
||||
ci.UserID = uid
|
||||
u, err := s.lookupUserFromID(uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
ci.User = u
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) lookupUserFromID(uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||
s.logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
// So make up a *user.User if their machine is broken in this way.
|
||||
return &user.User{
|
||||
Uid: uid,
|
||||
Username: "unknown-user-" + uid,
|
||||
Name: "unknown user " + uid,
|
||||
}, nil
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
// blockWhileInUse blocks while until either a Read from conn fails
|
||||
// (i.e. it's closed) or until the server is able to accept ci as a
|
||||
// user.
|
||||
func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, conn)
|
||||
close(connDone)
|
||||
}()
|
||||
ch := make(chan struct{}, 1)
|
||||
s.registerDisconnectSub(ch, true)
|
||||
defer s.registerDisconnectSub(ch, false)
|
||||
for {
|
||||
select {
|
||||
case <-connDone:
|
||||
s.logf("blocked client Read completed; connIdentity=%v", ci)
|
||||
return
|
||||
case <-ch:
|
||||
s.mu.Lock()
|
||||
err := s.checkConnIdentityLocked(ci)
|
||||
s.mu.Unlock()
|
||||
if err == nil {
|
||||
s.logf("unblocking client, server is free; connIdentity=%v", ci)
|
||||
// Server is now available again for a new user.
|
||||
// TODO(bradfitz): keep this connection alive. But for
|
||||
// now just return and have our caller close the connection
|
||||
// (which unblocks the io.Copy goroutine we started above)
|
||||
// and then the client (e.g. Windows) will reconnect and
|
||||
// discover that it works.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) (err error) {
|
||||
runDone := make(chan error, 1)
|
||||
defer func() { runDone <- err }()
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
peek, _ := br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
isHTTPReq := string(peek) == "GET "
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
if err != nil {
|
||||
if isHTTPReq {
|
||||
fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error())
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
_, occupied := err.(inUseOtherUserError)
|
||||
if occupied {
|
||||
bs.SendInUseOtherUserErrorMessage(err.Error())
|
||||
s.blockWhileInUse(c, ci)
|
||||
} else {
|
||||
bs.SendErrorMessage(err.Error())
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
s.b.SetCurrentUserID(ci.UserID)
|
||||
|
||||
if isHTTPReq {
|
||||
httpServer := http.Server{
|
||||
// Localhost connections are cheap; so only do
|
||||
// keep-alives for a short period of time, as these
|
||||
// active connections lock the server into only serving
|
||||
// that user. If the user has this page open, we don't
|
||||
// want another switching user to be locked out for
|
||||
// minutes. 5 seconds is enough to let browser hit
|
||||
// favicon.ico and such.
|
||||
IdleTimeout: 5 * time.Second,
|
||||
ErrorLog: logger.StdLogger(logf),
|
||||
Handler: s.localhostHandler(ci),
|
||||
}
|
||||
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
|
||||
return
|
||||
}
|
||||
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
logf("[v1] ReadMsg: %v", err)
|
||||
} else if ctx.Err() == nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.bsMu.Lock()
|
||||
if err := s.bs.GotCommandMsg(msg); err != nil {
|
||||
logf("GotCommandMsg: %v", err)
|
||||
}
|
||||
gotQuit := s.bs.GotQuit
|
||||
s.bsMu.Unlock()
|
||||
if gotQuit {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inUseOtherUserError is the error type for when the server is in use
|
||||
// by a different local user.
|
||||
type inUseOtherUserError struct{ error }
|
||||
|
||||
func (e inUseOtherUserError) Unwrap() error { return e.error }
|
||||
|
||||
// checkConnIdentityLocked checks whether the provided identity is
|
||||
// allowed to connect to the server.
|
||||
//
|
||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
// If clients are already connected, verify they're the same user.
|
||||
// This mostly matters on Windows at the moment.
|
||||
if len(s.allClients) > 0 {
|
||||
var active connIdentity
|
||||
for _, active = range s.allClients {
|
||||
break
|
||||
}
|
||||
if ci.UserID != active.UserID {
|
||||
//lint:ignore ST1005 we want to capitalize Tailscale here
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)}
|
||||
}
|
||||
}
|
||||
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
|
||||
//lint:ignore ST1005 we want to capitalize Tailscale here
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if add {
|
||||
if s.disconnectSub == nil {
|
||||
s.disconnectSub = make(map[chan<- struct{}]struct{})
|
||||
}
|
||||
s.disconnectSub[ch] = struct{}{}
|
||||
} else {
|
||||
delete(s.disconnectSub, ch)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// addConn adds c to the server's list of clients.
|
||||
//
|
||||
// If the returned error is of type inUseOtherUserError then the
|
||||
// returned connIdentity is also valid.
|
||||
func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
ci, err = s.getConnIdentity(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If the connected user changes, reset the backend server state to make
|
||||
// sure node keys don't leak between users.
|
||||
var doReset bool
|
||||
defer func() {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.clients == nil {
|
||||
s.clients = map[net.Conn]bool{}
|
||||
}
|
||||
if s.allClients == nil {
|
||||
s.allClients = map[net.Conn]connIdentity{}
|
||||
}
|
||||
|
||||
if err := s.checkConnIdentityLocked(ci); err != nil {
|
||||
return ci, err
|
||||
}
|
||||
|
||||
if !isHTTP {
|
||||
s.clients[c] = true
|
||||
}
|
||||
s.allClients[c] = ci
|
||||
|
||||
if s.lastUserID != ci.UserID {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = ci.UserID
|
||||
}
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
s.mu.Lock()
|
||||
delete(s.clients, c)
|
||||
delete(s.allClients, c)
|
||||
remain := len(s.allClients)
|
||||
for sub := range s.disconnectSub {
|
||||
select {
|
||||
case sub <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
if s.b.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func (s *server) stopAll() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for c := range s.clients {
|
||||
safesocket.ConnCloseRead(c)
|
||||
safesocket.ConnCloseWrite(c)
|
||||
}
|
||||
s.clients = nil
|
||||
}
|
||||
|
||||
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
|
||||
//
|
||||
// s.mu must be held
|
||||
func (s *server) setServerModeUserLocked() {
|
||||
var ci connIdentity
|
||||
var ok bool
|
||||
for _, ci = range s.allClients {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if !ok {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
return
|
||||
}
|
||||
if ci.User != nil {
|
||||
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
|
||||
s.serverModeUser = ci.User
|
||||
} else {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) writeToClients(b []byte) {
|
||||
inServerMode := s.b.InServerMode()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if inServerMode {
|
||||
if s.serverModeUser == nil {
|
||||
s.setServerModeUserLocked()
|
||||
}
|
||||
} else {
|
||||
if s.serverModeUser != nil {
|
||||
s.logf("ipnserver: no longer in server mode")
|
||||
s.serverModeUser = nil
|
||||
}
|
||||
}
|
||||
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs a Tailscale backend service.
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
|
||||
listen, _, err := safesocket.Listen(opts.SocketPath, uint16(opts.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
// Go listeners can't take a context, close it instead.
|
||||
server := &server{
|
||||
logf: logf,
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
}
|
||||
|
||||
// When the context is closed or when we return, whichever is first, close our listner
|
||||
// and all open connections.
|
||||
go func() {
|
||||
select {
|
||||
case <-rctx.Done():
|
||||
case <-ctx.Done():
|
||||
case <-runDone:
|
||||
}
|
||||
server.stopAll()
|
||||
listen.Close()
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
store, err = ipn.NewFileStore(opts.StatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
|
||||
}
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
|
||||
}
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
uid := strings.TrimPrefix(key, "user-")
|
||||
u, err := server.lookupUserFromID(uid)
|
||||
if err != nil {
|
||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
||||
} else {
|
||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
||||
server.serverModeUser = u
|
||||
}
|
||||
opts.AutostartStateKey = ipn.StateKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, e)
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
defer b.Shutdown()
|
||||
b.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return zstd.NewReader(nil)
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
|
||||
if opts.DebugMux != nil {
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
}
|
||||
|
||||
var s net.Conn
|
||||
serverToClient := func(b []byte) {
|
||||
if s != nil { // TODO: racy access to s?
|
||||
ipn.WriteMsg(s, b)
|
||||
}
|
||||
}
|
||||
|
||||
bs := ipn.NewBackendServer(logf, b, serverToClient)
|
||||
server.b = b
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
bs.GotCommand(&ipn.Command{
|
||||
Version: version.LONG,
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
StateKey: opts.AutostartStateKey,
|
||||
@@ -146,57 +592,33 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
oldS net.Conn
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
stopAll := func() {
|
||||
// Currently we only support one client connection at a time.
|
||||
// Theoretically we could allow multiple clients, by passing
|
||||
// notifications to all of them and accepting commands from
|
||||
// any of them, but there doesn't seem to be much need for
|
||||
// that right now.
|
||||
if oldS != nil {
|
||||
cancel()
|
||||
safesocket.ConnCloseRead(oldS)
|
||||
safesocket.ConnCloseWrite(oldS)
|
||||
systemd.Ready()
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
var c net.Conn
|
||||
var err error
|
||||
if unservedConn != nil {
|
||||
c = unservedConn
|
||||
unservedConn = nil
|
||||
} else {
|
||||
c, err = listen.Accept()
|
||||
}
|
||||
}
|
||||
|
||||
bo := backoff.Backoff{Name: "ipnserver"}
|
||||
|
||||
for i := 1; rctx.Err() == nil; i++ {
|
||||
s, err = listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(rctx, err)
|
||||
if ctx.Err() == nil {
|
||||
logf("ipnserver: Accept: %v", err)
|
||||
bo.BackOff(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
logf("%d: Incoming control connection.", i)
|
||||
stopAll()
|
||||
|
||||
ctx, cancel = context.WithCancel(rctx)
|
||||
oldS = s
|
||||
|
||||
go func(ctx context.Context, s net.Conn, i int) {
|
||||
logf := logger.WithPrefix(logf, fmt.Sprintf("%d: ", i))
|
||||
pump(logf, ctx, bs, s)
|
||||
if !opts.SurviveDisconnects || bs.GotQuit {
|
||||
bs.Reset()
|
||||
s.Close()
|
||||
}
|
||||
// Quitting not allowed, just keep going.
|
||||
bs.GotQuit = false
|
||||
}(ctx, s, i)
|
||||
|
||||
bo.BackOff(ctx, nil)
|
||||
go server.serveConn(ctx, c, logger.WithPrefix(logf, fmt.Sprintf("ipnserver: conn%d: ", i)))
|
||||
}
|
||||
stopAll()
|
||||
|
||||
return rctx.Err()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// BabysitProc runs the current executable as a child process with the
|
||||
// provided args, capturing its output, writing it to files, and
|
||||
// restarting the process on any crashes.
|
||||
//
|
||||
// It's only currently (2020-10-29) used on Windows.
|
||||
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
|
||||
executable, err := os.Executable()
|
||||
@@ -204,6 +626,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(args) != 2 && args[0] != "/subproc" {
|
||||
panic(fmt.Sprintf("unexpected arguments %q", args))
|
||||
}
|
||||
logID := args[1]
|
||||
logf = filelogger.New("tailscale-service", logID, logf)
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
@@ -229,7 +659,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
proc.mu.Unlock()
|
||||
}()
|
||||
|
||||
bo := backoff.Backoff{Name: "BabysitProc"}
|
||||
bo := backoff.NewBackoff("BabysitProc", logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
startTime := time.Now()
|
||||
@@ -298,6 +728,10 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
// pipe. We'll make a new one when we restart the subproc.
|
||||
wStdin.Close()
|
||||
|
||||
if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" {
|
||||
log.Fatalf("Process ended.")
|
||||
}
|
||||
|
||||
if time.Since(startTime) < 60*time.Second {
|
||||
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
|
||||
} else {
|
||||
@@ -312,3 +746,74 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FixedEngine returns a func that returns eng and a nil error.
|
||||
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
|
||||
// it's already had a few bytes read from it to determine that it's
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
// server it's closed, so the server can account the who's connected.
|
||||
type protoSwitchConn struct {
|
||||
s *server
|
||||
net.Conn
|
||||
br *bufio.Reader
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (psc *protoSwitchConn) Read(p []byte) (int, error) { return psc.br.Read(p) }
|
||||
func (psc *protoSwitchConn) Close() error {
|
||||
psc.closeOnce.Do(func() { psc.s.removeAndCloseConn(psc.Conn) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.Unknown {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
serveHTMLStatus(w, s.b)
|
||||
})
|
||||
}
|
||||
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
}
|
||||
|
||||
func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
for _, e := range entries {
|
||||
if e.Local == ra && e.Remote == la {
|
||||
return e.Pid
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ package ipnserver_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -25,11 +23,7 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
td, err := ioutil.TempDir("", "TestRunMultipleAccepts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
td := t.TempDir()
|
||||
socketPath := filepath.Join(td, "tailscale.sock")
|
||||
|
||||
logf := func(format string, args ...interface{}) {
|
||||
@@ -72,6 +66,6 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
SocketPath: socketPath,
|
||||
}
|
||||
t.Logf("pre-Run")
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", opts, eng)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -25,8 +26,11 @@ import (
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.Public {
|
||||
@@ -41,6 +45,7 @@ func (s *Status) Peers() []key.Public {
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
@@ -49,14 +54,22 @@ type PeerStatus struct {
|
||||
// Endpoints:
|
||||
Addrs []string
|
||||
CurAddr string // one of Addrs, or unique if roaming
|
||||
Relay string // DERP region
|
||||
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
|
||||
// ShareeNode indicates this node exists in the netmap because
|
||||
// it's owned by a shared-to user and that node might connect
|
||||
// to us. These nodes should be hidden by "tailscale status"
|
||||
// etc by default.
|
||||
ShareeNode bool `json:",omitempty"`
|
||||
|
||||
// InNetworkMap means that this peer was seen in our latest network map.
|
||||
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
|
||||
InNetworkMap bool
|
||||
@@ -84,6 +97,12 @@ type StatusBuilder struct {
|
||||
st Status
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@@ -91,6 +110,13 @@ func (sb *StatusBuilder) Status() *Status {
|
||||
return &sb.st
|
||||
}
|
||||
|
||||
// SetSelfStatus sets the status of the local machine.
|
||||
func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.Self = ss
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.mu.Lock()
|
||||
@@ -107,6 +133,18 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.st.User[id] = up
|
||||
}
|
||||
|
||||
// AddIP adds a Tailscale IP address to the status.
|
||||
func (sb *StatusBuilder) AddTailscaleIP(ip netaddr.IP) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
if sb.locked {
|
||||
log.Printf("[unexpected] ipnstate: AddIP after Locked")
|
||||
return
|
||||
}
|
||||
|
||||
sb.st.TailscaleIPs = append(sb.st.TailscaleIPs, ip)
|
||||
}
|
||||
|
||||
// AddPeer adds a peer node to the status.
|
||||
//
|
||||
// Its PeerStatus is mixed with any previous status already added.
|
||||
@@ -135,6 +173,12 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
if v := st.DNSName; v != "" {
|
||||
e.DNSName = v
|
||||
}
|
||||
if v := st.Relay; v != "" {
|
||||
e.Relay = v
|
||||
}
|
||||
if v := st.UserID; v != 0 {
|
||||
e.UserID = v
|
||||
}
|
||||
@@ -165,6 +209,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if v := st.LastSeen; !v.IsZero() {
|
||||
e.LastSeen = v
|
||||
}
|
||||
if v := st.LastWrite; !v.IsZero() {
|
||||
e.LastWrite = v
|
||||
}
|
||||
if st.InNetworkMap {
|
||||
e.InNetworkMap = true
|
||||
}
|
||||
@@ -177,6 +224,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
@@ -186,35 +236,50 @@ type StatusUpdater interface {
|
||||
func (st *Status) WriteHTML(w io.Writer) {
|
||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
||||
|
||||
f(`<html><head><style>
|
||||
.owner { font-size: 80%%; color: #444; }
|
||||
.tailaddr { font-size: 80%%; font-family: monospace: }
|
||||
</style></head>`)
|
||||
f("<body><h1>Tailscale State</h1>")
|
||||
f(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Tailscale State</title>
|
||||
<style>
|
||||
body { font-family: monospace; }
|
||||
.owner { text-decoration: underline; }
|
||||
.tailaddr { font-style: italic; }
|
||||
.acenter { text-align: center; }
|
||||
.aright { text-align: right; }
|
||||
table, th, td { border: 1px solid black; border-spacing : 0; border-collapse : collapse; }
|
||||
thead { background-color: #FFA500; }
|
||||
th, td { padding: 5px; }
|
||||
td { vertical-align: top; }
|
||||
table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tailscale State</h1>
|
||||
`)
|
||||
|
||||
//f("<p><b>logid:</b> %s</p>\n", logid)
|
||||
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
|
||||
|
||||
f("<table border=1 cellpadding=5><tr><th>Peer</th><th>Node</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>")
|
||||
ips := make([]string, 0, len(st.TailscaleIPs))
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// The tailcontrol server rounds LastSeen to 10 minutes. So we
|
||||
// declare that a longAgo seen time of 15 minutes means
|
||||
// they're not connected.
|
||||
longAgo := now.Add(-15 * time.Minute)
|
||||
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
var hsAgo string
|
||||
if !ps.LastHandshake.IsZero() {
|
||||
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago"
|
||||
} else {
|
||||
if ps.LastSeen.Before(longAgo) {
|
||||
hsAgo = "<i>offline</i>"
|
||||
} else if !ps.KeepAlive {
|
||||
hsAgo = "on demand"
|
||||
} else {
|
||||
hsAgo = "<b>pending</b>"
|
||||
var actAgo string
|
||||
if !ps.LastWrite.IsZero() {
|
||||
ago := now.Sub(ps.LastWrite)
|
||||
actAgo = ago.Round(time.Second).String() + " ago"
|
||||
if ago < 5*time.Minute {
|
||||
actAgo = "<b>" + actAgo + "</b>"
|
||||
}
|
||||
}
|
||||
var owner string
|
||||
@@ -224,33 +289,46 @@ func (st *Status) WriteHTML(w io.Writer) {
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
f("<tr><td>%s</td><td>%s<div class=owner>%s</div><div class=tailaddr>%s</div></td><td>%v</td><td>%v</td><td>%v</td>",
|
||||
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
peer.ShortString(),
|
||||
osEmoji(ps.OS)+" "+html.EscapeString(ps.SimpleHostName()),
|
||||
html.EscapeString(owner),
|
||||
html.EscapeString(ps.SimpleHostName()),
|
||||
osEmoji(ps.OS),
|
||||
ps.TailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
hsAgo,
|
||||
actAgo,
|
||||
)
|
||||
f("<td>")
|
||||
f("<td class=\"aright\">")
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
relay := ps.Relay
|
||||
if relay != "" {
|
||||
if active && ps.CurAddr == "" {
|
||||
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
|
||||
} else {
|
||||
f("derp-%v<br>", html.EscapeString(relay))
|
||||
}
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, addr := range ps.Addrs {
|
||||
if addr == ps.CurAddr {
|
||||
match = true
|
||||
f("<b>%s</b> 🔗<br>\n", addr)
|
||||
f("🔗 <b>%s</b><br>", addr)
|
||||
} else {
|
||||
f("%s<br>\n", addr)
|
||||
f("%s<br>", addr)
|
||||
}
|
||||
}
|
||||
if ps.CurAddr != "" && !match {
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>\n", ps.CurAddr)
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
|
||||
}
|
||||
f("</tr>") // end Addrs
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
}
|
||||
f("</table>")
|
||||
f("</tbody>\n</table>\n")
|
||||
f("</body>\n</html>\n")
|
||||
}
|
||||
|
||||
func osEmoji(os string) string {
|
||||
@@ -272,3 +350,21 @@ func osEmoji(os string) string {
|
||||
}
|
||||
return "👽"
|
||||
}
|
||||
|
||||
// PingResult contains response information for the "tailscale ping" subcommand,
|
||||
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
|
||||
type PingResult struct {
|
||||
IP string // ping destination
|
||||
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
|
||||
NodeName string // DNS name base or (possibly not unique) hostname
|
||||
|
||||
Err string
|
||||
LatencySeconds float64
|
||||
|
||||
Endpoint string // ip:port if direct UDP was used
|
||||
|
||||
DERPRegionID int // non-zero if DERP was used
|
||||
DERPRegionCode string // three-letter airport/region code if DERP was used
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
1351
ipn/local.go
1351
ipn/local.go
File diff suppressed because it is too large
Load Diff
91
ipn/loglines_test.go
Normal file
91
ipn/loglines_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// TestLocalLogLines tests to make sure that the log lines required for log parsing are
|
||||
// being logged by the expected functions. Update these tests if moving log lines between
|
||||
// functions.
|
||||
func TestLocalLogLines(t *testing.T) {
|
||||
logListen := tstest.NewLogLineTracker(t.Logf, []string{
|
||||
"SetPrefs: %v",
|
||||
"[v1] peer keys: %s",
|
||||
"[v1] v%v peers: %v",
|
||||
})
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
for i := 0; i < len(ret); i++ {
|
||||
ret[i] = hex
|
||||
}
|
||||
return ret
|
||||
}
|
||||
idA := logid(0xaa)
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lb, err := NewLocalBackend(logListen.Logf, idA.String(), store, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lb.Shutdown()
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = NewPrefs()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
|
||||
testWantRemain := func(wantRemain ...string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if remain := logListen.Check(); !reflect.DeepEqual(remain, wantRemain) {
|
||||
t.Errorf("remain %q, want %q", remain, wantRemain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log prefs line
|
||||
persist := &controlclient.Persist{}
|
||||
prefs := NewPrefs()
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
}},
|
||||
LocalAddrs: []string{"idk an address"},
|
||||
}
|
||||
lb.mu.Lock()
|
||||
lb.parseWgStatusLocked(status)
|
||||
lb.mu.Unlock()
|
||||
|
||||
t.Run("after_peers", testWantRemain())
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -13,11 +14,14 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
type NoArgs struct{}
|
||||
|
||||
type StartArgs struct {
|
||||
@@ -32,21 +36,35 @@ type FakeExpireAfterArgs struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type PingArgs struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
// Command is a command message that is JSON encoded and sent by a
|
||||
// frontend to a backend.
|
||||
type Command struct {
|
||||
_ structs.Incomparable
|
||||
_ structs.Incomparable
|
||||
|
||||
// Version is the binary version of the frontend (the client).
|
||||
Version string
|
||||
|
||||
// AllowVersionSkew controls whether it's permitted for the
|
||||
// client and server to have a different version. The default
|
||||
// (false) means to be strict.
|
||||
AllowVersionSkew bool
|
||||
|
||||
// Exactly one of the following must be non-nil.
|
||||
Quit *NoArgs
|
||||
Start *StartArgs
|
||||
StartLoginInteractive *NoArgs
|
||||
Login *oauth2.Token
|
||||
Logout *NoArgs
|
||||
SetPrefs *SetPrefsArgs
|
||||
SetWantRunning *bool
|
||||
RequestEngineStatus *NoArgs
|
||||
RequestStatus *NoArgs
|
||||
FakeExpireAfter *FakeExpireAfterArgs
|
||||
Ping *PingArgs
|
||||
}
|
||||
|
||||
type BackendServer struct {
|
||||
@@ -65,14 +83,32 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte))
|
||||
}
|
||||
|
||||
func (bs *BackendServer) send(n Notify) {
|
||||
n.Version = version.LONG
|
||||
n.Version = version.Long
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) SendErrorMessage(msg string) {
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
}
|
||||
|
||||
// SendInUseOtherUserErrorMessage sends a Notify message to the client that
|
||||
// both sets the state to 'InUseOtherUser' and sets the associated reason
|
||||
// to msg.
|
||||
func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
||||
inUse := InUseOtherUser
|
||||
bs.send(Notify{
|
||||
State: &inUse,
|
||||
ErrMessage: &msg,
|
||||
})
|
||||
}
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
@@ -87,14 +123,14 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
cmd.Version = version.LONG
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
if cmd.Version != version.LONG {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
cmd.Version, version.LONG)
|
||||
cmd.Version, version.Long)
|
||||
bs.logf("%s", vs)
|
||||
// ignore the command, but send a message back to the
|
||||
// caller so it can realize the version mismatch too.
|
||||
@@ -117,12 +153,18 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.StartLoginInteractive; c != nil {
|
||||
bs.b.StartLoginInteractive()
|
||||
return nil
|
||||
} else if c := cmd.Login; c != nil {
|
||||
bs.b.Login(c)
|
||||
return nil
|
||||
} else if c := cmd.Logout; c != nil {
|
||||
bs.b.Logout()
|
||||
return nil
|
||||
} else if c := cmd.SetPrefs; c != nil {
|
||||
bs.b.SetPrefs(c.New)
|
||||
return nil
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
@@ -132,6 +174,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
@@ -147,6 +192,10 @@ type BackendClient struct {
|
||||
logf logger.Logf
|
||||
sendCommandMsg func(jsonb []byte)
|
||||
notify func(Notify)
|
||||
|
||||
// AllowVersionSkew controls whether to allow mismatched
|
||||
// frontend & backend versions.
|
||||
AllowVersionSkew bool
|
||||
}
|
||||
|
||||
func NewBackendClient(logf logger.Logf, sendCommandMsg func(jsonb []byte)) *BackendClient {
|
||||
@@ -161,13 +210,16 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
// not interesting
|
||||
return
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
|
||||
}
|
||||
n := Notify{}
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
|
||||
}
|
||||
if n.Version != version.LONG {
|
||||
if n.Version != version.Long && !bc.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
|
||||
version.LONG, n.Version)
|
||||
version.Long, n.Version)
|
||||
bc.logf("%s", vs)
|
||||
// delete anything in the notification except the version,
|
||||
// to prevent incorrect operation.
|
||||
@@ -182,11 +234,14 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
}
|
||||
|
||||
func (bc *BackendClient) send(cmd Command) {
|
||||
cmd.Version = version.LONG
|
||||
cmd.Version = version.Long
|
||||
b, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
|
||||
}
|
||||
bc.sendCommandMsg(b)
|
||||
}
|
||||
|
||||
@@ -210,6 +265,10 @@ func (bc *BackendClient) StartLoginInteractive() {
|
||||
bc.send(Command{StartLoginInteractive: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Login(token *oauth2.Token) {
|
||||
bc.send(Command{Login: token})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Logout() {
|
||||
bc.send(Command{Logout: &NoArgs{}})
|
||||
}
|
||||
@@ -223,15 +282,23 @@ func (bc *BackendClient) RequestEngineStatus() {
|
||||
}
|
||||
|
||||
func (bc *BackendClient) RequestStatus() {
|
||||
bc.send(Command{RequestStatus: &NoArgs{}})
|
||||
bc.send(Command{AllowVersionSkew: true, RequestStatus: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Ping(ip string) {
|
||||
bc.send(Command{Ping: &PingArgs{IP: ip}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetWantRunning(v bool) {
|
||||
bc.send(Command{SetWantRunning: &v})
|
||||
}
|
||||
|
||||
// MaxMessageSize is the maximum message size, in bytes.
|
||||
const MaxMessageSize = 1 << 20
|
||||
const MaxMessageSize = 10 << 20
|
||||
|
||||
// TODO(apenwarr): incremental json decode?
|
||||
// That would let us avoid storing the whole byte array uselessly in RAM.
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestReadWrite(t *testing.T) {
|
||||
tstest.FixLogs(t)
|
||||
defer tstest.UnfixLogs(t)
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
@@ -62,8 +62,7 @@ func TestReadWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
tstest.FixLogs(t)
|
||||
defer tstest.UnfixLogs(t)
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
@@ -179,4 +178,10 @@ func TestClientServer(t *testing.T) {
|
||||
|
||||
h.Logout()
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.Login(&oauth2.Token{
|
||||
AccessToken: "google_id_token",
|
||||
TokenType: GoogleIDTokenType,
|
||||
})
|
||||
flushUntil(Running)
|
||||
}
|
||||
|
||||
156
ipn/prefs.go
156
ipn/prefs.go
@@ -5,48 +5,59 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
// Prefs are the user modifiable settings of the Tailscale node agent.
|
||||
type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnet and default routes
|
||||
// advertised by other nodes on the Tailscale network.
|
||||
RouteAll bool
|
||||
|
||||
// AllowSingleHosts specifies whether to install routes for each
|
||||
// node IP on the tailscale network, in addition to a route for
|
||||
// the whole network.
|
||||
// This corresponds to the "tailscale up --host-routes" value,
|
||||
// which defaults to true.
|
||||
//
|
||||
// TODO(danderson): why do we have this? It dumps a lot of stuff
|
||||
// into the routing table, and a single network route _should_ be
|
||||
// all that we need. But when I turn this off in my tailscaled,
|
||||
// packets stop flowing. What's up with that?
|
||||
AllowSingleHosts bool
|
||||
|
||||
// CorpDNS specifies whether to install the Tailscale network's
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
|
||||
// WantRunning indicates whether networking should be active on
|
||||
// this node.
|
||||
WantRunning bool
|
||||
|
||||
// ShieldsUp indicates whether to block all incoming connections,
|
||||
// regardless of the control-provided packet filter. If false, we
|
||||
// use the packet filter as provided. If true, we block incoming
|
||||
// connections.
|
||||
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
|
||||
ShieldsUp bool
|
||||
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
|
||||
// Tailscale network as reachable through the current node.
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
|
||||
// AdvertiseTags specifies groups that this node wants to join, for
|
||||
// purposes of ACL enforcement. These can be referenced from the ACL
|
||||
// security policy. Note that advertising a tag doesn't guarantee that
|
||||
@@ -54,6 +65,16 @@ type Prefs struct {
|
||||
// tag.
|
||||
AdvertiseTags []string
|
||||
|
||||
// Hostname is the hostname to use for identifying the node. If
|
||||
// not set, os.Hostname is used.
|
||||
Hostname string
|
||||
|
||||
// OSVersion overrides tailcfg.Hostinfo's OSVersion.
|
||||
OSVersion string
|
||||
|
||||
// DeviceModel overrides tailcfg.Hostinfo's DeviceModel.
|
||||
DeviceModel string
|
||||
|
||||
// NotepadURLs is a debugging setting that opens OAuth URLs in
|
||||
// notepad.exe on Windows, rather than loading them in a browser.
|
||||
//
|
||||
@@ -62,8 +83,40 @@ type Prefs struct {
|
||||
// users narrow it down a bit.
|
||||
NotepadURLs bool
|
||||
|
||||
// DisableDERP prevents DERP from being used.
|
||||
DisableDERP bool
|
||||
// ForceDaemon specifies whether a platform that normally
|
||||
// operates in "client mode" (that is, requires an active user
|
||||
// logged in with the GUI app running) should keep running after the
|
||||
// GUI ends and/or the user logs out.
|
||||
//
|
||||
// The only current applicable platform is Windows. This
|
||||
// forced Windows to go into "server mode" where Tailscale is
|
||||
// running even with no users logged in. This might also be
|
||||
// used for macOS in the future. This setting has no effect
|
||||
// for Linux/etc, which always operate in daemon mode.
|
||||
ForceDaemon bool `json:"ForceDaemon,omitempty"`
|
||||
|
||||
// The following block of options only have an effect on Linux.
|
||||
|
||||
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
|
||||
// Tailscale network as reachable through the current
|
||||
// node.
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
|
||||
// NoSNAT specifies whether to source NAT traffic going to
|
||||
// destinations in AdvertiseRoutes. The default is to apply source
|
||||
// NAT, which makes the traffic appear to come from the router
|
||||
// machine rather than the peer's Tailscale IP.
|
||||
//
|
||||
// Disabling SNAT requires additional manual configuration in your
|
||||
// network to route Tailscale traffic back to the subnet relay
|
||||
// machine.
|
||||
//
|
||||
// Linux-only.
|
||||
NoSNAT bool
|
||||
|
||||
// NetfilterMode specifies how much to manage netfilter rules for
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode router.NetfilterMode
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
@@ -76,16 +129,46 @@ type Prefs struct {
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
|
||||
|
||||
func (p *Prefs) Pretty() string {
|
||||
var pp string
|
||||
if p.Persist != nil {
|
||||
pp = p.Persist.Pretty()
|
||||
} else {
|
||||
pp = "Persist=nil"
|
||||
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
|
||||
func (p *Prefs) pretty(goos string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Prefs{")
|
||||
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
|
||||
if !p.AllowSingleHosts {
|
||||
sb.WriteString("mesh=false ")
|
||||
}
|
||||
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v derp=%v shields=%v routes=%v %v}",
|
||||
p.RouteAll, p.AllowSingleHosts, p.CorpDNS, p.WantRunning,
|
||||
p.NotepadURLs, !p.DisableDERP, p.ShieldsUp, p.AdvertiseRoutes, pp)
|
||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||
if p.ForceDaemon {
|
||||
sb.WriteString("server=true ")
|
||||
}
|
||||
if p.NotepadURLs {
|
||||
sb.WriteString("notepad=true ")
|
||||
}
|
||||
if p.ShieldsUp {
|
||||
sb.WriteString("shields=true ")
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
|
||||
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
|
||||
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
|
||||
}
|
||||
if len(p.AdvertiseTags) > 0 {
|
||||
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
||||
}
|
||||
if goos == "linux" {
|
||||
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
||||
}
|
||||
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
|
||||
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
|
||||
}
|
||||
if p.Persist != nil {
|
||||
sb.WriteString(p.Persist.Pretty())
|
||||
} else {
|
||||
sb.WriteString("Persist=nil")
|
||||
}
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (p *Prefs) ToBytes() []byte {
|
||||
@@ -111,19 +194,24 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
p.DisableDERP == p2.DisableDERP &&
|
||||
p.ShieldsUp == p2.ShieldsUp &&
|
||||
p.NoSNAT == p2.NoSNAT &&
|
||||
p.NetfilterMode == p2.NetfilterMode &&
|
||||
p.Hostname == p2.Hostname &&
|
||||
p.OSVersion == p2.OSVersion &&
|
||||
p.DeviceModel == p2.DeviceModel &&
|
||||
p.ForceDaemon == p2.ForceDaemon &&
|
||||
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
|
||||
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
||||
p.Persist.Equals(p2.Persist)
|
||||
}
|
||||
|
||||
func compareIPNets(a, b []wgcfg.CIDR) bool {
|
||||
func compareIPNets(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if !a[i].IP.Equal(b[i].IP) || a[i].Mask != b[i].Mask {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -152,6 +240,7 @@ func NewPrefs() *Prefs {
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
NetfilterMode: router.NetfilterOn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,27 +270,24 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of p.
|
||||
func (p *Prefs) Clone() *Prefs {
|
||||
// TODO: write a faster/non-Fatal-y Clone implementation?
|
||||
p2, err := PrefsFromBytes(p.ToBytes(), false)
|
||||
if err != nil {
|
||||
log.Fatalf("Prefs was uncopyable: %v\n", err)
|
||||
}
|
||||
return p2
|
||||
}
|
||||
|
||||
// LoadLegacyPrefs loads a legacy relaynode config file into Prefs
|
||||
// with sensible migration defaults set. If enforceDefaults is true,
|
||||
// Prefs.RouteAll and Prefs.AllowSingleHosts are forced on.
|
||||
func LoadPrefs(filename string, enforceDefaults bool) (*Prefs, error) {
|
||||
// LoadPrefs loads a legacy relaynode config file into Prefs
|
||||
// with sensible migration defaults set.
|
||||
func LoadPrefs(filename string) (*Prefs, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading prefs from %q: %v", filename, err)
|
||||
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
|
||||
}
|
||||
if bytes.Contains(data, jsonEscapedZero) {
|
||||
// Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
|
||||
// in the backend process that ended up sending NULL bytes over JSON
|
||||
// to the frontend which wrote them out to JSON files on disk.
|
||||
// So if we see one, treat is as corrupt and the user will need
|
||||
// to log in again. (better than crashing)
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
p, err := PrefsFromBytes(data, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding prefs in %q: %v", filename, err)
|
||||
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
51
ipn/prefs_clone.go
Normal file
51
ipn/prefs_clone.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Prefs; DO NOT EDIT.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Prefs) Clone() *Prefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Prefs)
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if dst.Persist != nil {
|
||||
dst.Persist = new(controlclient.Persist)
|
||||
*dst.Persist = *src.Persist
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Prefs
|
||||
var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
ShieldsUp bool
|
||||
AdvertiseTags []string
|
||||
Hostname string
|
||||
OSVersion string
|
||||
DeviceModel string
|
||||
NotepadURLs bool
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
}{})
|
||||
@@ -5,11 +5,19 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -20,15 +28,17 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
}
|
||||
|
||||
func TestPrefsEqual(t *testing.T) {
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseRoutes", "AdvertiseTags", "NotepadURLs", "DisableDERP", "Persist"}
|
||||
tstest.PanicOnLog()
|
||||
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, prefsHandles)
|
||||
}
|
||||
|
||||
nets := func(strs ...string) (ns []wgcfg.CIDR) {
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
for _, s := range strs {
|
||||
n, err := wgcfg.ParseCIDR(s)
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -111,6 +121,28 @@ func TestPrefsEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{NoSNAT: true},
|
||||
&Prefs{NoSNAT: false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{NoSNAT: true},
|
||||
&Prefs{NoSNAT: true},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{Hostname: "android-host01"},
|
||||
&Prefs{Hostname: "android-host02"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{Hostname: ""},
|
||||
&Prefs{Hostname: ""},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{NotepadURLs: true},
|
||||
&Prefs{NotepadURLs: false},
|
||||
@@ -135,12 +167,12 @@ func TestPrefsEqual(t *testing.T) {
|
||||
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: nil},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -159,6 +191,17 @@ func TestPrefsEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOff},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{Persist: &controlclient.Persist{}},
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
@@ -220,6 +263,8 @@ func checkPrefs(t *testing.T, p Prefs) {
|
||||
}
|
||||
|
||||
func TestBasicPrefs(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
p := Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
}
|
||||
@@ -227,6 +272,8 @@ func TestBasicPrefs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrefsPersist(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
c := controlclient.Persist{
|
||||
LoginName: "test@example.com",
|
||||
}
|
||||
@@ -237,3 +284,114 @@ func TestPrefsPersist(t *testing.T) {
|
||||
}
|
||||
checkPrefs(t, p)
|
||||
}
|
||||
|
||||
func TestPrefsPretty(t *testing.T) {
|
||||
tests := []struct {
|
||||
p Prefs
|
||||
os string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
Prefs{},
|
||||
"linux",
|
||||
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{ShieldsUp: true},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{AllowSingleHosts: true},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
NotepadURLs: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ForceDaemon: true, // server mode
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ControlURL: "http://localhost:1234",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
},
|
||||
"darwin",
|
||||
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.p.pretty(tt.os)
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrefsNotExist(t *testing.T) {
|
||||
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
|
||||
|
||||
p, err := LoadPrefs(bogusFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
|
||||
// See issue #954 for details.
|
||||
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := f.Name()
|
||||
if _, err := f.Write(jsonEscapedZero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
defer os.Remove(path)
|
||||
|
||||
p, err := LoadPrefs(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
42
ipn/store.go
42
ipn/store.go
@@ -5,9 +5,12 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -19,6 +22,30 @@ import (
|
||||
// requested state ID doesn't exist.
|
||||
var ErrStateNotExist = errors.New("no state with given ID")
|
||||
|
||||
const (
|
||||
// MachineKeyStateKey is the key under which we store the machine key,
|
||||
// in its wgkey.Private.MarshalText representation.
|
||||
MachineKeyStateKey = StateKey("_machinekey")
|
||||
|
||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||
// loads on startup.
|
||||
//
|
||||
// We have to support multiple state keys for other OSes (Windows in
|
||||
// particular), but right now Unix daemons run with a single
|
||||
// node-global state. To keep open the option of having per-user state
|
||||
// later, the global state key doesn't look like a username.
|
||||
GlobalDaemonStateKey = StateKey("_daemon")
|
||||
|
||||
// ServerModeStartKey's value, if non-empty, is the value of a
|
||||
// StateKey containing the prefs to start with which to start the
|
||||
// server.
|
||||
//
|
||||
// For example, the value might be "user-1234", meaning the
|
||||
// the server should start with the Prefs JSON loaded from
|
||||
// StateKey "user-1234".
|
||||
ServerModeStartKey = StateKey("server-mode-start-key")
|
||||
)
|
||||
|
||||
// StateStore persists state, and produces it back on request.
|
||||
type StateStore interface {
|
||||
// ReadState returns the bytes associated with ID. Returns (nil,
|
||||
@@ -34,6 +61,8 @@ type MemoryStore struct {
|
||||
cache map[StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *MemoryStore) String() string { return "MemoryStore" }
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
@@ -67,9 +96,19 @@ type FileStore struct {
|
||||
cache map[StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
|
||||
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
|
||||
if err == nil && len(bs) == 0 {
|
||||
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Write out an initial file, to verify that we can write
|
||||
@@ -112,6 +151,9 @@ func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
|
||||
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if bytes.Equal(s.cache[id], bs) {
|
||||
return nil
|
||||
}
|
||||
s.cache[id] = append([]byte(nil), bs...)
|
||||
bs, err := json.MarshalIndent(s.cache, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func testStoreSemantics(t *testing.T, store StateStore) {
|
||||
@@ -76,11 +78,15 @@ func testStoreSemantics(t *testing.T, store StateStore) {
|
||||
}
|
||||
|
||||
func TestMemoryStore(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
store := &MemoryStore{}
|
||||
testStoreSemantics(t, store)
|
||||
}
|
||||
|
||||
func TestFileStore(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
f, err := ioutil.TempFile("", "test_ipn_store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
199
log/filelogger/log.go
Normal file
199
log/filelogger/log.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package filelogger provides localdisk log writing & rotation, primarily for Windows
|
||||
// clients. (We get this for free on other platforms.)
|
||||
package filelogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSize = 100 << 20
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// files on Windows, rotating old log files as needed to stay under
|
||||
// file count & byte limits.
|
||||
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
}
|
||||
return lfw.Logf
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
type logFileWriter struct {
|
||||
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
|
||||
logID string // hex logID
|
||||
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
|
||||
wrappedLogf logger.Logf // underlying logger to send to
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
buf bytes.Buffer // scratch buffer to avoid allocs
|
||||
fday civilDay // day that f was opened; zero means no file yet open
|
||||
f *os.File // file currently opened for append
|
||||
}
|
||||
|
||||
// civilDay is a year, month, and day in the local timezone.
|
||||
// It's a comparable value type.
|
||||
type civilDay struct {
|
||||
year int
|
||||
month time.Month
|
||||
day int
|
||||
}
|
||||
|
||||
func dayOf(t time.Time) civilDay {
|
||||
return civilDay{t.Year(), t.Month(), t.Day()}
|
||||
}
|
||||
|
||||
func (w *logFileWriter) Logf(format string, a ...interface{}) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.buf.Reset()
|
||||
fmt.Fprintf(&w.buf, format, a...)
|
||||
if w.buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
out := w.buf.Bytes()
|
||||
w.wrappedLogf("%s", out)
|
||||
|
||||
// Make sure there's a final newline before we write to the log file.
|
||||
if out[len(out)-1] != '\n' {
|
||||
w.buf.WriteByte('\n')
|
||||
out = w.buf.Bytes()
|
||||
}
|
||||
|
||||
w.appendToFileLocked(out)
|
||||
}
|
||||
|
||||
// out should end in a newline.
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) appendToFileLocked(out []byte) {
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
if w.fday != day {
|
||||
w.startNewFileLocked()
|
||||
}
|
||||
if w.f != nil {
|
||||
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
|
||||
const formatPre = "2006-01-02T15:04:05"
|
||||
const formatPost = "Z07:00"
|
||||
fmt.Fprintf(w.f, "%s.%03d%s: %s",
|
||||
now.Format(formatPre),
|
||||
now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
|
||||
now.Format(formatPost),
|
||||
out)
|
||||
}
|
||||
}
|
||||
|
||||
// startNewFileLocked opens a new log file for writing
|
||||
// and also cleans up any old files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) startNewFileLocked() {
|
||||
var oldName string
|
||||
if w.f != nil {
|
||||
oldName = filepath.Base(w.f.Name())
|
||||
w.f.Close()
|
||||
w.f = nil
|
||||
w.fday = civilDay{}
|
||||
}
|
||||
w.cleanLocked()
|
||||
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
|
||||
w.fileBasePrefix,
|
||||
day.year,
|
||||
day.month,
|
||||
day.day,
|
||||
now.Hour(),
|
||||
now.Minute(),
|
||||
now.Second(),
|
||||
now.Unix()))
|
||||
var err error
|
||||
w.f, err = os.Create(name)
|
||||
if err != nil {
|
||||
w.wrappedLogf("failed to create log file: %v", err)
|
||||
return
|
||||
}
|
||||
if oldName != "" {
|
||||
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
|
||||
} else {
|
||||
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
|
||||
}
|
||||
w.fday = day
|
||||
}
|
||||
|
||||
// cleanLocked cleans up old log files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) cleanLocked() {
|
||||
fis, _ := ioutil.ReadDir(w.dir)
|
||||
prefix := w.fileBasePrefix + "-"
|
||||
fileSize := map[string]int64{}
|
||||
var files []string
|
||||
var sumSize int64
|
||||
for _, fi := range fis {
|
||||
baseName := filepath.Base(fi.Name())
|
||||
if !strings.HasPrefix(baseName, prefix) {
|
||||
continue
|
||||
}
|
||||
size := fi.Size()
|
||||
fileSize[baseName] = size
|
||||
sumSize += size
|
||||
files = append(files, baseName)
|
||||
}
|
||||
if sumSize > maxSize {
|
||||
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
|
||||
}
|
||||
if len(files) > maxFiles {
|
||||
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
|
||||
}
|
||||
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
|
||||
target := files[0]
|
||||
files = files[1:]
|
||||
|
||||
targetSize := fileSize[target]
|
||||
targetFull := filepath.Join(w.dir, target)
|
||||
err := os.Remove(targetFull)
|
||||
if err != nil {
|
||||
w.wrappedLogf("error cleaning log file: %v", err)
|
||||
} else {
|
||||
sumSize -= targetSize
|
||||
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
|
||||
}
|
||||
}
|
||||
}
|
||||
44
log/logheap/logheap.go
Normal file
44
log/logheap/logheap.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package logheap logs a heap pprof profile.
|
||||
package logheap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogHeap uploads a JSON logtail record with the base64 heap pprof by means
|
||||
// of an HTTP POST request to the endpoint referred to in postURL.
|
||||
func LogHeap(postURL string) {
|
||||
if postURL == "" {
|
||||
return
|
||||
}
|
||||
runtime.GC()
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pprof.WriteHeapProfile(buf); err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", postURL, buf)
|
||||
if err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
}
|
||||
@@ -18,18 +18,24 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -43,7 +49,7 @@ type Config struct {
|
||||
// Policy is a logger and its public ID.
|
||||
type Policy struct {
|
||||
// Logtail is the logger.
|
||||
Logtail logtail.Logger
|
||||
Logtail *logtail.Logger
|
||||
// PublicID is the logger's instance identifier.
|
||||
PublicID logtail.PublicID
|
||||
}
|
||||
@@ -52,7 +58,7 @@ type Policy struct {
|
||||
func (c *Config) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(c, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalf("logpolicy.Config marshal: %v\n", err)
|
||||
log.Fatalf("logpolicy.Config marshal: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -99,21 +105,48 @@ func (l logWriter) Write(buf []byte) (int, error) {
|
||||
|
||||
// logsDir returns the directory to use for log configuration and
|
||||
// buffer storage.
|
||||
func logsDir() string {
|
||||
systemdCacheDir := os.Getenv("CACHE_DIRECTORY")
|
||||
if systemdCacheDir != "" {
|
||||
return systemdCacheDir
|
||||
func logsDir(logf logger.Logf) string {
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
if systemdStateDir != "" {
|
||||
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
|
||||
return systemdStateDir
|
||||
}
|
||||
|
||||
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
|
||||
if d := paths.DefaultTailscaledStateFile(); d != "" {
|
||||
d = filepath.Dir(d) // directory of e.g. "/var/lib/tailscale/tailscaled.state"
|
||||
if err := os.MkdirAll(d, 0700); err == nil {
|
||||
logf("logpolicy: using system state directory %q", d)
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err == nil {
|
||||
return filepath.Join(cacheDir, "Tailscale")
|
||||
d := filepath.Join(cacheDir, "Tailscale")
|
||||
logf("logpolicy: using UserCacheDir, %q", d)
|
||||
return d
|
||||
}
|
||||
|
||||
// No idea where to put stuff. This only happens when $HOME is
|
||||
// unset, which os.UserCacheDir doesn't like. Use the current
|
||||
// working directory and hope for the best.
|
||||
return ""
|
||||
// Use the current working directory, unless we're being run by a
|
||||
// service manager that sets it to /.
|
||||
wd, err := os.Getwd()
|
||||
if err == nil && wd != "/" {
|
||||
logf("logpolicy: using current directory, %q", wd)
|
||||
return wd
|
||||
}
|
||||
|
||||
// No idea where to put stuff. Try to create a temp dir. It'll
|
||||
// mean we might lose some logs and rotate through log IDs, but
|
||||
// it's something.
|
||||
tmp, err := ioutil.TempDir("", "tailscaled-log-*")
|
||||
if err != nil {
|
||||
panic("no safe place found to store log state")
|
||||
}
|
||||
logf("logpolicy: using temp directory, %q", tmp)
|
||||
return tmp
|
||||
}
|
||||
|
||||
// runningUnderSystemd reports whether we're running under systemd.
|
||||
@@ -125,15 +158,167 @@ func runningUnderSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// tryFixLogStateLocation is a temporary fixup for
|
||||
// https://github.com/tailscale/tailscale/issues/247 . We accidentally
|
||||
// wrote logging state files to /, and then later to $CACHE_DIRECTORY
|
||||
// (which is incorrect because the log ID is not reconstructible if
|
||||
// deleted - it's state, not cache data).
|
||||
//
|
||||
// If log state for cmdname exists in / or $CACHE_DIRECTORY, and no
|
||||
// log state for that command exists in dir, then the log state is
|
||||
// moved from whereever it does exist, into dir. Leftover logs state
|
||||
// in / and $CACHE_DIRECTORY is deleted.
|
||||
func tryFixLogStateLocation(dir, cmdname string) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd":
|
||||
// These are the OSes where we might have written stuff into
|
||||
// root. Others use different logic to find the logs storage
|
||||
// dir.
|
||||
default:
|
||||
return
|
||||
}
|
||||
if cmdname == "" {
|
||||
log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
|
||||
return
|
||||
}
|
||||
if dir == "/" {
|
||||
// Trying to store things in / still. That's a bug, but don't
|
||||
// abort hard.
|
||||
log.Printf("[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale")
|
||||
return
|
||||
}
|
||||
if os.Getuid() != 0 {
|
||||
// Only root could have written log configs to weird places.
|
||||
return
|
||||
}
|
||||
|
||||
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
|
||||
// (aka /var/cache/tailscale). We want to move files into the
|
||||
// provided dir, preferring those in CACHE_DIR over those in / if
|
||||
// both exist. If files already exist in dir, don't
|
||||
// overwrite. Finally, once we've maybe moved files around, we
|
||||
// want to delete leftovers in / and CACHE_DIR, to clean up after
|
||||
// our past selves.
|
||||
|
||||
files := []string{
|
||||
fmt.Sprintf("%s.log.conf", cmdname),
|
||||
fmt.Sprintf("%s.log1.txt", cmdname),
|
||||
fmt.Sprintf("%s.log2.txt", cmdname),
|
||||
}
|
||||
|
||||
// checks if any of the files above exist in d.
|
||||
checkExists := func(d string) (bool, error) {
|
||||
for _, file := range files {
|
||||
p := filepath.Join(d, file)
|
||||
_, err := os.Stat(p)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("stat %q: %w", p, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
// move files from d into dir, if they exist.
|
||||
moveFiles := func(d string) error {
|
||||
for _, file := range files {
|
||||
src := filepath.Join(d, file)
|
||||
_, err := os.Stat(src)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("stat %q: %v", src, err)
|
||||
}
|
||||
dst := filepath.Join(dir, file)
|
||||
bs, err := exec.Command("mv", src, dst).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("mv %q %q: %v (%s)", src, dst, err, bs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
existsInRoot, err := checkExists("/")
|
||||
if err != nil {
|
||||
log.Printf("checking for configs in /: %v", err)
|
||||
return
|
||||
}
|
||||
existsInCache := false
|
||||
cacheDir := os.Getenv("CACHE_DIRECTORY")
|
||||
if cacheDir != "" {
|
||||
existsInCache, err = checkExists("/var/cache/tailscale")
|
||||
if err != nil {
|
||||
log.Printf("checking for configs in %s: %v", cacheDir, err)
|
||||
}
|
||||
}
|
||||
existsInDest, err := checkExists(dir)
|
||||
if err != nil {
|
||||
log.Printf("checking for configs in %s: %v", dir, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case !existsInRoot && !existsInCache:
|
||||
// No leftover files, nothing to do.
|
||||
return
|
||||
case existsInDest:
|
||||
// Already have "canonical" configs, just delete any remnants
|
||||
// (below).
|
||||
case existsInCache:
|
||||
// CACHE_DIRECTORY takes precedence over /, move files from
|
||||
// there.
|
||||
if err := moveFiles(cacheDir); err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
case existsInRoot:
|
||||
// Files from root is better than nothing.
|
||||
if err := moveFiles("/"); err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If moving succeeded, or we didn't need to move files, try to
|
||||
// delete any leftover files, but it's okay if we can't delete
|
||||
// them for some reason.
|
||||
dirs := []string{}
|
||||
if existsInCache {
|
||||
dirs = append(dirs, cacheDir)
|
||||
}
|
||||
if existsInRoot {
|
||||
dirs = append(dirs, "/")
|
||||
}
|
||||
for _, d := range dirs {
|
||||
for _, file := range files {
|
||||
p := filepath.Join(d, file)
|
||||
_, err := os.Stat(p)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Printf("stat %q: %v", p, err)
|
||||
return
|
||||
}
|
||||
if err := os.Remove(p); err != nil {
|
||||
log.Printf("rm %q: %v", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new log policy (a logger and its instance ID) for a
|
||||
// given collection name.
|
||||
func New(collection string) *Policy {
|
||||
var lflags int
|
||||
if terminal.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
if term.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
lflags = 0
|
||||
} else {
|
||||
lflags = log.LstdFlags
|
||||
}
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_TIME")); v {
|
||||
lflags = log.LstdFlags | log.Lmicroseconds
|
||||
}
|
||||
if runningUnderSystemd() {
|
||||
// If journalctl is going to prepend its own timestamp
|
||||
// anyway, no need to add one.
|
||||
@@ -141,18 +326,28 @@ func New(collection string) *Policy {
|
||||
}
|
||||
console := log.New(stderrWriter{}, "", lflags)
|
||||
|
||||
dir := logsDir()
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", version.CmdName()))
|
||||
var earlyErrBuf bytes.Buffer
|
||||
earlyLogf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&earlyErrBuf, format, a...)
|
||||
earlyErrBuf.WriteByte('\n')
|
||||
}
|
||||
|
||||
dir := logsDir(earlyLogf)
|
||||
|
||||
cmdName := version.CmdName()
|
||||
tryFixLogStateLocation(dir, cmdName)
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
|
||||
var oldc *Config
|
||||
data, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
log.Printf("logpolicy.Read %v: %v\n", cfgPath, err)
|
||||
earlyLogf("logpolicy.Read %v: %v", cfgPath, err)
|
||||
oldc = &Config{}
|
||||
oldc.Collection = collection
|
||||
} else {
|
||||
oldc, err = ConfigFromBytes(data)
|
||||
if err != nil {
|
||||
log.Printf("logpolicy.Config unmarshal: %v\n", err)
|
||||
earlyLogf("logpolicy.Config unmarshal: %v", err)
|
||||
oldc = &Config{}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +360,7 @@ func New(collection string) *Policy {
|
||||
newc.PrivateID = logtail.PrivateID{}
|
||||
newc.Collection = collection
|
||||
}
|
||||
if newc.PrivateID == (logtail.PrivateID{}) {
|
||||
if newc.PrivateID.IsZero() {
|
||||
newc.PrivateID, err = logtail.NewPrivateID()
|
||||
if err != nil {
|
||||
log.Fatalf("logpolicy: NewPrivateID() should never fail")
|
||||
@@ -174,7 +369,7 @@ func New(collection string) *Policy {
|
||||
newc.PublicID = newc.PrivateID.Public()
|
||||
if newc != *oldc {
|
||||
if err := newc.save(cfgPath); err != nil {
|
||||
log.Printf("logpolicy.Config.Save: %v\n", err)
|
||||
earlyLogf("logpolicy.Config.Save: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +378,7 @@ func New(collection string) *Policy {
|
||||
PrivateID: newc.PrivateID,
|
||||
Stderr: logWriter{console},
|
||||
NewZstdEncoder: func() logtail.Encoder {
|
||||
w, err := zstd.NewWriter(nil)
|
||||
w, err := smallzstd.NewEncoder(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -192,22 +387,25 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, version.CmdName()), filch.Options{})
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
}
|
||||
lw := logtail.Log(c)
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v\n",
|
||||
version.LONG,
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
goVersion(),
|
||||
os.Args)
|
||||
log.Printf("LogID: %v\n", newc.PublicID)
|
||||
log.Printf("LogID: %v", newc.PublicID)
|
||||
if filchErr != nil {
|
||||
log.Printf("filch failed: %v", filchErr)
|
||||
}
|
||||
if earlyErrBuf.Len() != 0 {
|
||||
log.Printf("%s", earlyErrBuf.Bytes())
|
||||
}
|
||||
|
||||
return &Policy{
|
||||
Logtail: lw,
|
||||
@@ -215,6 +413,15 @@ func New(collection string) *Policy {
|
||||
}
|
||||
}
|
||||
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
//
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (p *Policy) SetVerbosityLevel(level int) {
|
||||
p.Logtail.SetVerbosityLevel(level)
|
||||
}
|
||||
|
||||
// Close immediately shuts down the logger.
|
||||
func (p *Policy) Close() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -226,7 +433,7 @@ func (p *Policy) Close() {
|
||||
// log upload if it can be done before ctx is canceled.
|
||||
func (p *Policy) Shutdown(ctx context.Context) error {
|
||||
if p.Logtail != nil {
|
||||
log.Printf("flushing log.\n")
|
||||
log.Printf("flushing log.")
|
||||
return p.Logtail.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
@@ -238,6 +445,9 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
// Start with a copy of http.DefaultTransport and tweak it a bit.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
|
||||
// We do our own zstd compression on uploads, and responses never contain any payload,
|
||||
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
|
||||
// will never be any body to decompress:
|
||||
@@ -245,11 +455,10 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
|
||||
// Log whenever we dial:
|
||||
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
nd := &net.Dialer{
|
||||
nd := netns.FromDialer(&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
})
|
||||
t0 := time.Now()
|
||||
c, err := nd.DialContext(ctx, netw, addr)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
@@ -280,3 +489,11 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func goVersion() string {
|
||||
v := strings.TrimPrefix(runtime.Version(), "go")
|
||||
if racebuild.On {
|
||||
return v + "-race"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -2,57 +2,81 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package backoff provides a back-off timer type.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const MAX_BACKOFF_MSEC = 30000
|
||||
|
||||
// Backoff tracks state the history of consecutive failures and sleeps
|
||||
// an increasing amount of time, up to a provided limit.
|
||||
type Backoff struct {
|
||||
n int
|
||||
n int // number of consecutive failures
|
||||
maxBackoff time.Duration
|
||||
|
||||
// Name is the name of this backoff timer, for logging purposes.
|
||||
Name string
|
||||
// NewTimer is the function that acts like time.NewTimer().
|
||||
// You can override this in unit tests.
|
||||
NewTimer func(d time.Duration) *time.Timer
|
||||
name string
|
||||
// logf is the function used for log messages when backing off.
|
||||
logf logger.Logf
|
||||
|
||||
// NewTimer is the function that acts like time.NewTimer.
|
||||
// It's for use in unit tests.
|
||||
NewTimer func(time.Duration) *time.Timer
|
||||
|
||||
// LogLongerThan sets the minimum time of a single backoff interval
|
||||
// before we mention it in the log.
|
||||
LogLongerThan time.Duration
|
||||
}
|
||||
|
||||
func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
if ctx.Err() == nil && err != nil {
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
msec := b.n * b.n * 10
|
||||
if msec > MAX_BACKOFF_MSEC {
|
||||
msec = MAX_BACKOFF_MSEC
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
msec = rand.Intn(msec) + msec/2
|
||||
dur := time.Duration(msec) * time.Millisecond
|
||||
if dur >= b.LogLongerThan {
|
||||
log.Printf("%s: backoff: %d msec\n", b.Name, msec)
|
||||
}
|
||||
newTimer := b.NewTimer
|
||||
if newTimer == nil {
|
||||
newTimer = time.NewTimer
|
||||
}
|
||||
t := newTimer(dur)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
} else {
|
||||
// not a regular error
|
||||
b.n = 0
|
||||
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
|
||||
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
|
||||
// are logged unless the returned Backoff.LogLongerThan is adjusted.
|
||||
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
|
||||
return &Backoff{
|
||||
name: name,
|
||||
logf: logf,
|
||||
maxBackoff: maxBackoff,
|
||||
NewTimer: time.NewTimer,
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff sleeps an increasing amount of time if err is non-nil.
|
||||
// and the context is not a
|
||||
// It resets the backoff schedule once err is nil.
|
||||
func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
if err == nil {
|
||||
// No error. Reset number of consecutive failures.
|
||||
b.n = 0
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// Fast path.
|
||||
return
|
||||
}
|
||||
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
|
||||
if d > b.maxBackoff {
|
||||
d = b.maxBackoff
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
if d >= b.LogLongerThan {
|
||||
b.logf("%s: backoff: %d msec", b.name, d.Milliseconds())
|
||||
}
|
||||
t := b.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ func main() {
|
||||
log.Fatalf("logtail: bad -privateid: %v", err)
|
||||
}
|
||||
|
||||
logger := logtail.Log(logtail.Config{
|
||||
logger := logtail.NewLogger(logtail.Config{
|
||||
Collection: *collection,
|
||||
PrivateID: id,
|
||||
})
|
||||
}, log.Printf)
|
||||
log.SetOutput(io.MultiWriter(logger, os.Stdout))
|
||||
defer logger.Flush()
|
||||
defer log.Printf("logtail exited")
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
@@ -56,19 +56,8 @@ func (f *filchTest) close(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func genFilePrefix(t *testing.T) string {
|
||||
t.Helper()
|
||||
filePrefix, err := ioutil.TempDir("", "filch")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return filepath.Join(filePrefix, "ringbuffer-")
|
||||
}
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(filepath.Dir(filePrefix))
|
||||
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
|
||||
f.readEOF(t)
|
||||
@@ -90,8 +79,7 @@ func TestQueue(t *testing.T) {
|
||||
|
||||
func TestRecover(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(filepath.Dir(filePrefix))
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.read(t, "hello")
|
||||
@@ -104,8 +92,7 @@ func TestRecover(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("cur", func(t *testing.T) {
|
||||
filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(filepath.Dir(filePrefix))
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.close(t)
|
||||
@@ -123,8 +110,7 @@ func TestRecover(t *testing.T) {
|
||||
filch_test.go:129: r.ReadLine()="hello", want "world"
|
||||
*/
|
||||
|
||||
filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(filepath.Dir(filePrefix))
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.read(t, "hello")
|
||||
@@ -143,6 +129,14 @@ func TestRecover(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFilchStderr(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO(bradfitz): this is broken on Windows but not
|
||||
// fully sure why. Investigate. But notably, the
|
||||
// stderrFD variable (defined in filch.go) and set
|
||||
// below is only ever read in filch_unix.go. So just
|
||||
// skip this for test for now.
|
||||
t.Skip("test broken on Windows")
|
||||
}
|
||||
pipeR, pipeW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -155,8 +149,7 @@ func TestFilchStderr(t *testing.T) {
|
||||
stderrFD = 2
|
||||
}()
|
||||
|
||||
filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(filepath.Dir(filePrefix))
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: true})
|
||||
f.write(t, "hello")
|
||||
if _, err := fmt.Fprintf(pipeW, "filch\n"); err != nil {
|
||||
|
||||
@@ -14,43 +14,17 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
// Config.BaseURL isn't provided.
|
||||
const DefaultHost = "log.tailscale.io"
|
||||
|
||||
type Logger interface {
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
// then contents is fit into a JSON blob and written.
|
||||
//
|
||||
// This is intended as an interface for the stdlib "log" package.
|
||||
Write([]byte) (int, error)
|
||||
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable error.
|
||||
Flush() error
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
// It will block, continuing to try and upload unless the passed
|
||||
// context object interrupts it by being done.
|
||||
// If the shutdown is interrupted, an error is returned.
|
||||
Shutdown(context.Context) error
|
||||
|
||||
// Close shuts down this logger object, the background log uploader
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// DEPRECATED: use Shutdown
|
||||
Close()
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
EncodeAll(src, dst []byte) []byte
|
||||
Close() error
|
||||
@@ -65,15 +39,16 @@ type Config struct {
|
||||
LowMemory bool // if true, logtail minimizes memory use
|
||||
TimeNow func() time.Time // if set, subsitutes uses of time.Now
|
||||
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
|
||||
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
|
||||
|
||||
// DrainLogs, if non-nil, disables autmatic uploading of new logs,
|
||||
// DrainLogs, if non-nil, disables automatic uploading of new logs,
|
||||
// so that logs are only uploaded when a token is sent to DrainLogs.
|
||||
DrainLogs <-chan struct{}
|
||||
}
|
||||
|
||||
func Log(cfg Config) Logger {
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://" + DefaultHost
|
||||
}
|
||||
@@ -93,8 +68,9 @@ func Log(cfg Config) Logger {
|
||||
}
|
||||
cfg.Buffer = NewMemoryBuffer(pendingSize)
|
||||
}
|
||||
l := &logger{
|
||||
l := &Logger{
|
||||
stderr: cfg.Stderr,
|
||||
stderrLevel: cfg.StderrLevel,
|
||||
httpc: cfg.HTTPC,
|
||||
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
|
||||
lowMem: cfg.LowMemory,
|
||||
@@ -104,9 +80,7 @@ func Log(cfg Config) Logger {
|
||||
sentinel: make(chan int32, 16),
|
||||
drainLogs: cfg.DrainLogs,
|
||||
timeNow: cfg.TimeNow,
|
||||
bo: backoff.Backoff{
|
||||
Name: "logtail",
|
||||
},
|
||||
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
|
||||
|
||||
shutdownStart: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
@@ -123,8 +97,11 @@ func Log(cfg Config) Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
// Logger writes logs, splitting them as configured between local
|
||||
// logging facilities and uploading to a log server.
|
||||
type Logger struct {
|
||||
stderr io.Writer
|
||||
stderrLevel int
|
||||
httpc *http.Client
|
||||
url string
|
||||
lowMem bool
|
||||
@@ -134,7 +111,7 @@ type logger struct {
|
||||
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
|
||||
sentinel chan int32
|
||||
timeNow func() time.Time
|
||||
bo backoff.Backoff
|
||||
bo *backoff.Backoff
|
||||
zstdEncoder Encoder
|
||||
uploadCancel func()
|
||||
|
||||
@@ -142,7 +119,22 @@ type logger struct {
|
||||
shutdownDone chan struct{} // closd when shutdown complete
|
||||
}
|
||||
|
||||
func (l *logger) Shutdown(ctx context.Context) error {
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
//
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (l *Logger) SetVerbosityLevel(level int) {
|
||||
l.stderrLevel = level
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
// It will block, continuing to try and upload unless the passed
|
||||
// context object interrupts it by being done.
|
||||
// If the shutdown is interrupted, an error is returned.
|
||||
func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
@@ -164,7 +156,11 @@ func (l *logger) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *logger) Close() {
|
||||
// Close shuts down this logger object, the background log uploader
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// Deprecated: use Shutdown
|
||||
func (l *Logger) Close() {
|
||||
l.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
@@ -175,7 +171,7 @@ func (l *logger) Close() {
|
||||
//
|
||||
// If the caller provides a DrainLogs channel, then unblock-drain-on-Write
|
||||
// is disabled, and it is up to the caller to trigger unblock the drain.
|
||||
func (l *logger) drainBlock() (shuttingDown bool) {
|
||||
func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
if l.drainLogs == nil {
|
||||
select {
|
||||
case <-l.shutdownStart:
|
||||
@@ -194,7 +190,7 @@ func (l *logger) drainBlock() (shuttingDown bool) {
|
||||
|
||||
// drainPending drains and encodes a batch of logs from the buffer for upload.
|
||||
// If no logs are available, drainPending blocks until logs are available.
|
||||
func (l *logger) drainPending() (res []byte) {
|
||||
func (l *Logger) drainPending() (res []byte) {
|
||||
buf := new(bytes.Buffer)
|
||||
entries := 0
|
||||
|
||||
@@ -255,13 +251,22 @@ func (l *logger) drainPending() (res []byte) {
|
||||
}
|
||||
|
||||
// This is the goroutine that repeatedly uploads logs in the background.
|
||||
func (l *logger) uploading(ctx context.Context) {
|
||||
func (l *Logger) uploading(ctx context.Context) {
|
||||
defer close(l.shutdownDone)
|
||||
|
||||
for {
|
||||
body := l.drainPending()
|
||||
if l.zstdEncoder != nil {
|
||||
body = l.zstdEncoder.EncodeAll(body, nil)
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if l.zstdEncoder != nil && len(body) > 256 {
|
||||
zbody := l.zstdEncoder.EncodeAll(body, nil)
|
||||
// Only send it compressed if the bandwidth savings are sufficient.
|
||||
// Just the extra headers associated with enabling compression
|
||||
// are 50 bytes by themselves.
|
||||
if len(body)-len(zbody) > 64 {
|
||||
origlen = len(body)
|
||||
body = zbody
|
||||
}
|
||||
}
|
||||
|
||||
for len(body) > 0 {
|
||||
@@ -270,14 +275,14 @@ func (l *logger) uploading(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
uploaded, err := l.upload(ctx, body)
|
||||
uploaded, err := l.upload(ctx, body, origlen)
|
||||
if err != nil {
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
}
|
||||
l.bo.BackOff(ctx, err)
|
||||
if uploaded {
|
||||
break
|
||||
}
|
||||
l.bo.BackOff(ctx, err)
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -288,7 +293,10 @@ func (l *logger) uploading(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err error) {
|
||||
// upload uploads body to the log server.
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
|
||||
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// I know of no conditions under which this could fail.
|
||||
@@ -296,8 +304,9 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
// TODO record logs to disk
|
||||
panic("logtail: cannot build http request: " + err.Error())
|
||||
}
|
||||
if l.zstdEncoder != nil {
|
||||
if origlen != -1 {
|
||||
req.Header.Add("Content-Encoding", "zstd")
|
||||
req.Header.Add("Orig-Content-Length", strconv.Itoa(origlen))
|
||||
}
|
||||
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
|
||||
|
||||
@@ -307,7 +316,7 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
compressedNote := "not-compressed"
|
||||
if l.zstdEncoder != nil {
|
||||
if origlen != -1 {
|
||||
compressedNote = "compressed"
|
||||
}
|
||||
|
||||
@@ -334,11 +343,13 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *logger) Flush() error {
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable error.
|
||||
func (l *Logger) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *logger) send(jsonBlob []byte) (int, error) {
|
||||
func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
n, err := l.buffer.Write(jsonBlob)
|
||||
if l.drainLogs == nil {
|
||||
select {
|
||||
@@ -351,7 +362,7 @@ func (l *logger) send(jsonBlob []byte) (int, error) {
|
||||
|
||||
// TODO: instead of allocating, this should probably just append
|
||||
// directly into the output log buffer.
|
||||
func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
now := l.timeNow()
|
||||
|
||||
// Factor in JSON encoding overhead to try to only do one alloc
|
||||
@@ -407,7 +418,7 @@ func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *logger) encode(buf []byte) []byte {
|
||||
func (l *Logger) encode(buf []byte) []byte {
|
||||
if buf[0] != '{' {
|
||||
return l.encodeText(buf, l.skipClientTime) // text fast-path
|
||||
}
|
||||
@@ -448,11 +459,18 @@ func (l *logger) encode(buf []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *logger) Write(buf []byte) (int, error) {
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
// then contents is fit into a JSON blob and written.
|
||||
//
|
||||
// This is intended as an interface for the stdlib "log" package.
|
||||
func (l *Logger) Write(buf []byte) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard {
|
||||
level, buf := parseAndRemoveLogLevel(buf)
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
|
||||
if buf[len(buf)-1] == '\n' {
|
||||
l.stderr.Write(buf)
|
||||
} else {
|
||||
@@ -463,5 +481,26 @@ func (l *logger) Write(buf []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
b := l.encode(buf)
|
||||
return l.send(b)
|
||||
_, err := l.send(b)
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
var (
|
||||
openBracketV = []byte("[v")
|
||||
v1 = []byte("[v1] ")
|
||||
v2 = []byte("[v2] ")
|
||||
)
|
||||
|
||||
// level 0 is normal (or unknown) level; 1+ are increasingly verbose
|
||||
func parseAndRemoveLogLevel(buf []byte) (level int, cleanBuf []byte) {
|
||||
if len(buf) == 0 || buf[0] == '{' || !bytes.Contains(buf, openBracketV) {
|
||||
return 0, buf
|
||||
}
|
||||
if bytes.Contains(buf, v1) {
|
||||
return 1, bytes.ReplaceAll(buf, v1, nil)
|
||||
}
|
||||
if bytes.Contains(buf, v2) {
|
||||
return 2, bytes.ReplaceAll(buf, v2, nil)
|
||||
}
|
||||
return 0, buf
|
||||
}
|
||||
|
||||
@@ -14,16 +14,16 @@ func TestFastShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
l := Log(Config{
|
||||
l := NewLogger(Config{
|
||||
BaseURL: "http://localhost:1234",
|
||||
})
|
||||
}, t.Logf)
|
||||
l.Shutdown(ctx)
|
||||
}
|
||||
|
||||
var sink []byte
|
||||
|
||||
func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
lg := &logger{timeNow: time.Now}
|
||||
lg := &Logger{timeNow: time.Now}
|
||||
inBuf := []byte("some text to encode")
|
||||
n := testing.AllocsPerRun(1000, func() {
|
||||
sink = lg.encodeText(inBuf, false)
|
||||
@@ -32,3 +32,18 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
t.Logf("allocs = %d; want 1", int(n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerWriteLength(t *testing.T) {
|
||||
lg := &Logger{
|
||||
timeNow: time.Now,
|
||||
buffer: NewMemoryBuffer(1024),
|
||||
}
|
||||
inBuf := []byte("some text to encode")
|
||||
n, err := lg.Write(inBuf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if n != len(inBuf) {
|
||||
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,3 +40,10 @@ func (m *LabelMap) Get(key string) *expvar.Int {
|
||||
m.Add(key, 0)
|
||||
return m.Map.Get(key).(*expvar.Int)
|
||||
}
|
||||
|
||||
// GetFloat returns a direct pointer to the expvar.Float for key, creating it
|
||||
// if necessary.
|
||||
func (m *LabelMap) GetFloat(key string) *expvar.Float {
|
||||
m.AddFloat(key, 0.0)
|
||||
return m.Map.Get(key).(*expvar.Float)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ package dnscache
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +28,7 @@ func preferGoResolver() bool {
|
||||
// There does not appear to be a local resolver running
|
||||
// on iOS, and NetworkExtension is good at isolating DNS.
|
||||
// So do not use the Go resolver on macOS/iOS.
|
||||
if runtime.GOOS == "darwin" {
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,8 +45,6 @@ func preferGoResolver() bool {
|
||||
// Get returns a caching Resolver singleton.
|
||||
func Get() *Resolver { return single }
|
||||
|
||||
const fixedTTL = 10 * time.Minute
|
||||
|
||||
// Resolver is a minimal DNS caching resolver.
|
||||
//
|
||||
// The TTL is always fixed for now. It's not intended for general use.
|
||||
@@ -54,6 +55,15 @@ type Resolver struct {
|
||||
// If nil, net.DefaultResolver is used.
|
||||
Forward *net.Resolver
|
||||
|
||||
// TTL is how long to keep entries cached
|
||||
//
|
||||
// If zero, a default (currently 10 minutes) is used.
|
||||
TTL time.Duration
|
||||
|
||||
// UseLastGood controls whether a cached entry older than TTL is used
|
||||
// if a refresh fails.
|
||||
UseLastGood bool
|
||||
|
||||
sf singleflight.Group
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -72,16 +82,31 @@ func (r *Resolver) fwd() *net.Resolver {
|
||||
return net.DefaultResolver
|
||||
}
|
||||
|
||||
func (r *Resolver) ttl() time.Duration {
|
||||
if r.TTL > 0 {
|
||||
return r.TTL
|
||||
}
|
||||
return 10 * time.Minute
|
||||
}
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
|
||||
// LookupIP returns the first IPv4 address found, otherwise the first IPv6 address.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
@@ -95,10 +120,24 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, res.Err
|
||||
}
|
||||
return res.Val.(net.IP), nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -112,12 +151,41 @@ func (r *Resolver) lookupIPCache(host string) (ip net.IP, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
if r.UseLastGood {
|
||||
if _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
// If we have some previous good value for this host,
|
||||
// don't give this DNS lookup much time. If we're in a
|
||||
// situation where the user's DNS server is unreachable
|
||||
// (e.g. their corp DNS server is behind a subnet router
|
||||
// that can't come up due to Tailscale needing to
|
||||
// connect to itself), then we want to fail fast and let
|
||||
// our caller (who set UseLastGood) fall back to using
|
||||
// the last-known-good IP address.
|
||||
return 3 * time.Second
|
||||
}
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
@@ -129,19 +197,26 @@ func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
|
||||
for _, ipa := range ips {
|
||||
if ip4 := ipa.IP.To4(); ip4 != nil {
|
||||
return r.addIPCache(host, ip4, fixedTTL), nil
|
||||
return r.addIPCache(host, ip4, r.ttl()), nil
|
||||
}
|
||||
}
|
||||
return r.addIPCache(host, ips[0].IP, fixedTTL), nil
|
||||
return r.addIPCache(host, ips[0].IP, r.ttl()), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
|
||||
if isPrivateIP(ip) {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.ipCache == nil {
|
||||
@@ -168,3 +243,26 @@ var (
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
)
|
||||
|
||||
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
ip, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,20 @@ package interfaces
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
// LoginEndpointForProxyDetermination is the URL used for testing
|
||||
// which HTTP proxy the system should use.
|
||||
var LoginEndpointForProxyDetermination = "https://login.tailscale.com/"
|
||||
|
||||
// Tailscale returns the current machine's Tailscale interface, if any.
|
||||
// If none is found, all zero values are returned.
|
||||
// A non-nil error is only returned on a problem listing the system interfaces.
|
||||
@@ -37,43 +47,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// HaveIPv6GlobalAddress reports whether the machine appears to have a
|
||||
// global scope unicast IPv6 address.
|
||||
//
|
||||
// It only returns an error if there's a problem querying the system
|
||||
// interfaces.
|
||||
func HaveIPv6GlobalAddress() (bool, error) {
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for i := range ifs {
|
||||
iface := &ifs[i]
|
||||
if !isUp(iface) || isLoopback(iface) {
|
||||
continue
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
ipnet, ok := a.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipnet.IP.To4() != nil || !ipnet.IP.IsGlobalUnicast() {
|
||||
continue
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// maybeTailscaleInterfaceName reports whether s is an interface
|
||||
// name that might be used by Tailscale.
|
||||
func maybeTailscaleInterfaceName(s string) bool {
|
||||
return strings.HasPrefix(s, "wg") ||
|
||||
return s == "Tailscale" ||
|
||||
strings.HasPrefix(s, "wg") ||
|
||||
strings.HasPrefix(s, "ts") ||
|
||||
strings.HasPrefix(s, "tailscale") ||
|
||||
strings.HasPrefix(s, "utun")
|
||||
@@ -82,7 +60,8 @@ func maybeTailscaleInterfaceName(s string) bool {
|
||||
// IsTailscaleIP reports whether ip is an IP in a range used by
|
||||
// Tailscale virtual network interfaces.
|
||||
func IsTailscaleIP(ip net.IP) bool {
|
||||
return cgNAT.Contains(ip)
|
||||
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
||||
return tsaddr.IsTailscaleIP(nip)
|
||||
}
|
||||
|
||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||
@@ -111,10 +90,8 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
// TODO(crawshaw): IPv6 support.
|
||||
// Easy to do here, but we need good endpoint ordering logic.
|
||||
ip := v.IP.To4()
|
||||
if ip == nil {
|
||||
ip, ok := netaddr.FromStdIP(v.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// TODO(apenwarr): don't special case cgNAT.
|
||||
@@ -122,7 +99,7 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
// very well be something we can route to
|
||||
// directly, because both nodes are
|
||||
// behind the same CGNAT router.
|
||||
if cgNAT.Contains(ip) {
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
continue
|
||||
}
|
||||
if linkLocalIPv4.Contains(ip) {
|
||||
@@ -148,7 +125,7 @@ func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
||||
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
||||
|
||||
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
||||
func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
||||
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,7 +139,9 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
fn(Interface{iface}, v.IP)
|
||||
if ip, ok := netaddr.FromStdIP(v.IP); ok {
|
||||
fn(Interface{iface}, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,46 +152,156 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
||||
// routing table, and other network configuration.
|
||||
// For now it's pretty basic.
|
||||
type State struct {
|
||||
InterfaceIPs map[string][]net.IP
|
||||
InterfaceIPs map[string][]netaddr.IP
|
||||
InterfaceUp map[string]bool
|
||||
|
||||
// HaveV6Global is whether this machine has an IPv6 global address
|
||||
// on some non-Tailscale interface that's up.
|
||||
HaveV6Global bool
|
||||
|
||||
// HaveV4 is whether the machine has some non-localhost,
|
||||
// non-link-local IPv4 address on a non-Tailscale interface that's up.
|
||||
HaveV4 bool
|
||||
|
||||
// IsExpensive is whether the current network interface is
|
||||
// considered "expensive", which currently means LTE/etc
|
||||
// instead of Wifi. This field is not populated by GetState.
|
||||
IsExpensive bool
|
||||
|
||||
// DefaultRouteInterface is the interface name for the machine's default route.
|
||||
// It is not yet populated on all OSes.
|
||||
// Its exact value should not be assumed to be a map key for
|
||||
// the Interface maps above; it's only used for debugging.
|
||||
DefaultRouteInterface string
|
||||
|
||||
// HTTPProxy is the HTTP proxy to use.
|
||||
HTTPProxy string
|
||||
|
||||
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
|
||||
PAC string
|
||||
}
|
||||
|
||||
func (s *State) String() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
|
||||
ifs := make([]string, 0, len(s.InterfaceUp))
|
||||
for k := range s.InterfaceUp {
|
||||
if allLoopbackIPs(s.InterfaceIPs[k]) {
|
||||
continue
|
||||
}
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
sort.Slice(ifs, func(i, j int) bool {
|
||||
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
|
||||
if upi != upj {
|
||||
// Up sorts before down.
|
||||
return upi
|
||||
}
|
||||
return ifs[i] < ifs[j]
|
||||
})
|
||||
for i, ifName := range ifs {
|
||||
if i > 0 {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
if s.InterfaceUp[ifName] {
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, ip := range s.InterfaceIPs[ifName] {
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
if needSpace {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s", ip)
|
||||
needSpace = true
|
||||
}
|
||||
sb.WriteString("]")
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "%s:down", ifName)
|
||||
}
|
||||
}
|
||||
sb.WriteString("}")
|
||||
|
||||
if s.IsExpensive {
|
||||
sb.WriteString(" expensive")
|
||||
}
|
||||
if s.HTTPProxy != "" {
|
||||
fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
|
||||
}
|
||||
if s.PAC != "" {
|
||||
fmt.Fprintf(&sb, " pac=%s", s.PAC)
|
||||
}
|
||||
fmt.Fprintf(&sb, " v4=%v v6global=%v}", s.HaveV4, s.HaveV6Global)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *State) Equal(s2 *State) bool {
|
||||
return reflect.DeepEqual(s, s2)
|
||||
}
|
||||
|
||||
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
|
||||
|
||||
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
|
||||
func (s *State) AnyInterfaceUp() bool {
|
||||
return s != nil && (s.HaveV4 || s.HaveV6Global)
|
||||
}
|
||||
|
||||
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
|
||||
// are owned by this process. (TODO: make this true; currently it
|
||||
// makes the Linux-only assumption that the interface is named
|
||||
// /^tailscale/)
|
||||
func (s *State) RemoveTailscaleInterfaces() {
|
||||
for name := range s.InterfaceIPs {
|
||||
if strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
if isTailscaleInterfaceName(name) {
|
||||
delete(s.InterfaceIPs, name)
|
||||
delete(s.InterfaceUp, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTailscaleInterfaceName(name string) bool {
|
||||
return name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
}
|
||||
|
||||
// getPAC, if non-nil, returns the current PAC file URL.
|
||||
var getPAC func() string
|
||||
|
||||
// GetState returns the state of all the current machine's network interfaces.
|
||||
//
|
||||
// It does not set the returned State.IsExpensive. The caller can populate that.
|
||||
func GetState() (*State, error) {
|
||||
s := &State{
|
||||
InterfaceIPs: make(map[string][]net.IP),
|
||||
InterfaceIPs: make(map[string][]netaddr.IP),
|
||||
InterfaceUp: make(map[string]bool),
|
||||
}
|
||||
if err := ForeachInterfaceAddress(func(ni Interface, ip net.IP) {
|
||||
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
|
||||
ifUp := ni.IsUp()
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
||||
s.InterfaceUp[ni.Name] = ni.IsUp()
|
||||
s.InterfaceUp[ni.Name] = ifUp
|
||||
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||
s.HaveV4 = s.HaveV4 || ip.Is4()
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
|
||||
s.HTTPProxy = u.String()
|
||||
}
|
||||
if getPAC != nil {
|
||||
s.PAC = getPAC()
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -227,7 +316,7 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, ip net.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
if isPrivateIP(ip) {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
@@ -246,22 +335,71 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
var likelyHomeRouterIP func() (netaddr.IP, bool)
|
||||
|
||||
// LikelyHomeRouterIP returns the likely IP of the residential router,
|
||||
// which will always be an IPv4 private address, if found.
|
||||
// In addition, it returns the IP address of the current machine on
|
||||
// the LAN using that gateway.
|
||||
// This is used as the destination for UPnP, NAT-PMP, PCP, etc queries.
|
||||
func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
if likelyHomeRouterIP != nil {
|
||||
gateway, ok = likelyHomeRouterIP()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, prefix := range privatev4s {
|
||||
if prefix.Contains(gateway) && prefix.Contains(ip) {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return gateway, myIP, !myIP.IsZero()
|
||||
}
|
||||
|
||||
func isPrivateIP(ip netaddr.IP) bool {
|
||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR(s)
|
||||
func isGlobalV6(ip netaddr.IP) bool {
|
||||
return v6Global1.Contains(ip)
|
||||
}
|
||||
|
||||
func mustCIDR(s string) netaddr.IPPrefix {
|
||||
prefix, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipNet
|
||||
return prefix
|
||||
}
|
||||
|
||||
var (
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
cgNAT = mustCIDR("100.64.0.0/10")
|
||||
privatev4s = []netaddr.IPPrefix{private1, private2, private3}
|
||||
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
)
|
||||
|
||||
func allLoopbackIPs(ips []netaddr.IP) bool {
|
||||
if len(ips) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if !ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
74
net/interfaces/interfaces_darwin.go
Normal file
74
net/interfaces/interfaces_darwin.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
|
||||
Internet:
|
||||
Destination Gateway Flags Netif Expire
|
||||
default 10.0.0.1 UGSc en0
|
||||
default link#14 UCSI utun2
|
||||
10/16 link#4 UCS en0 !
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
|
||||
*/
|
||||
func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Wait()
|
||||
|
||||
var f []mem.RO
|
||||
lineread.Reader(stdout, func(lineb []byte) error {
|
||||
line := mem.B(lineb)
|
||||
if !mem.Contains(line, mem.S("default")) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
124
net/interfaces/interfaces_darwin_cgo.go
Normal file
124
net/interfaces/interfaces_darwin_cgo.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,cgo
|
||||
|
||||
package interfaces
|
||||
|
||||
/*
|
||||
#import "route.h"
|
||||
#import <netinet/in.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <stdlib.h>
|
||||
#import <stdio.h>
|
||||
|
||||
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
|
||||
// Otherwise, it returns 0.
|
||||
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
{
|
||||
// sockaddrs are after the message header
|
||||
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
|
||||
|
||||
if((rtm->rtm_addrs & (RTA_DST|RTA_GATEWAY)) != (RTA_DST|RTA_GATEWAY))
|
||||
return 0; // missing dst or gateway addr
|
||||
if (dst_sa->sa_family != AF_INET)
|
||||
return 0; // dst not IPv4
|
||||
if ((rtm->rtm_flags & RTF_GATEWAY) == 0)
|
||||
return 0; // gateway flag not set
|
||||
|
||||
struct sockaddr_in* dst_si = (struct sockaddr_in *)dst_sa;
|
||||
if (dst_si->sin_addr.s_addr != INADDR_ANY)
|
||||
return 0; // not default route
|
||||
|
||||
#define ROUNDUP(a) ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long))
|
||||
|
||||
struct sockaddr* gateway_sa = (struct sockaddr *)((char *)dst_sa + ROUNDUP(dst_sa->sa_len));
|
||||
if (gateway_sa->sa_family != AF_INET)
|
||||
return 0; // gateway not IPv4
|
||||
|
||||
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
|
||||
int ip;
|
||||
ip = gateway_si->sin_addr.s_addr;
|
||||
|
||||
unsigned char a, b;
|
||||
a = (ip >> 0) & 0xff;
|
||||
b = (ip >> 8) & 0xff;
|
||||
|
||||
// Check whether ip is private, that is, whether it is
|
||||
// in one of 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.
|
||||
if (a == 10)
|
||||
return ip; // matches 10.0.0.0/8
|
||||
if (a == 172 && (b >> 4) == 1)
|
||||
return ip; // matches 172.16.0.0/12
|
||||
if (a == 192 && b == 168)
|
||||
return ip; // matches 192.168.0.0/16
|
||||
|
||||
// Not a private IP.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// privateGatewayIP returns the private gateway IP address, if it exists.
|
||||
// If no private gateway IP address was found, it returns 0.
|
||||
// On an error, it returns an error code in (0, 255].
|
||||
// Any private gateway IP address is > 255.
|
||||
int privateGatewayIP()
|
||||
{
|
||||
size_t needed;
|
||||
int mib[6];
|
||||
char *buf;
|
||||
|
||||
mib[0] = CTL_NET;
|
||||
mib[1] = PF_ROUTE;
|
||||
mib[2] = 0;
|
||||
mib[3] = 0;
|
||||
mib[4] = NET_RT_DUMP2;
|
||||
mib[5] = 0;
|
||||
|
||||
if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0)
|
||||
return 1; // route dump size estimation failed
|
||||
if ((buf = malloc(needed)) == 0)
|
||||
return 2; // malloc failed
|
||||
if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
|
||||
free(buf);
|
||||
return 3; // route dump failed
|
||||
}
|
||||
|
||||
// Loop over all routes.
|
||||
char *next, *lim;
|
||||
lim = buf + needed;
|
||||
struct rt_msghdr2 *rtm;
|
||||
for (next = buf; next < lim; next += rtm->rtm_msglen) {
|
||||
rtm = (struct rt_msghdr2 *)next;
|
||||
int ip;
|
||||
ip = privateGatewayIPFromRoute(rtm);
|
||||
if (ip) {
|
||||
free(buf);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
return 0; // no gateway found
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinSyscall
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
|
||||
ip := C.privateGatewayIP()
|
||||
if ip < 255 {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
var q [4]byte
|
||||
binary.LittleEndian.PutUint32(q[:], uint32(ip))
|
||||
return netaddr.IPv4(q[0], q[1], q[2], q[3]), true
|
||||
}
|
||||
20
net/interfaces/interfaces_darwin_cgo_test.go
Normal file
20
net/interfaces/interfaces_darwin_cgo_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build cgo,darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinSyscall()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
}
|
||||
11
net/interfaces/interfaces_darwin_nocgo.go
Normal file
11
net/interfaces/interfaces_darwin_nocgo.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!cgo
|
||||
|
||||
package interfaces
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinExec
|
||||
}
|
||||
15
net/interfaces/interfaces_defaultrouteif_todo.go
Normal file
15
net/interfaces/interfaces_defaultrouteif_todo.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows
|
||||
|
||||
package interfaces
|
||||
|
||||
import "errors"
|
||||
|
||||
var errTODO = errors.New("TODO")
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
return "TODO", errTODO
|
||||
}
|
||||
210
net/interfaces/interfaces_linux.go
Normal file
210
net/interfaces/interfaces_linux.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPLinux
|
||||
}
|
||||
|
||||
var procNetRouteErr syncs.AtomicBool
|
||||
|
||||
/*
|
||||
Parse 10.0.0.1 out of:
|
||||
|
||||
$ cat /proc/net/route
|
||||
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
ens18 00000000 0100000A 0003 0 0 0 00000000 0 0 0
|
||||
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
|
||||
*/
|
||||
func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
if procNetRouteErr.Get() {
|
||||
// If we failed to read /proc/net/route previously, don't keep trying.
|
||||
// But if we're on Android, go into the Android path.
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
return ret, false
|
||||
}
|
||||
lineNum := 0
|
||||
var f []mem.RO
|
||||
err := lineread.File("/proc/net/route", func(line []byte) error {
|
||||
lineNum++
|
||||
if lineNum == 1 {
|
||||
// Skip header line.
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], mem.B(line))
|
||||
if len(f) < 4 {
|
||||
return nil
|
||||
}
|
||||
gwHex, flagsHex := f[2], f[3]
|
||||
flags, err := mem.ParseUint(flagsHex, 16, 16)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
const RTF_UP = 0x0001
|
||||
const RTF_GATEWAY = 0x0002
|
||||
if flags&(RTF_UP|RTF_GATEWAY) != RTF_UP|RTF_GATEWAY {
|
||||
return nil
|
||||
}
|
||||
ipu32, err := mem.ParseUint(gwHex, 16, 32)
|
||||
if err != nil {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
|
||||
if isPrivateIP(ip) {
|
||||
ret = ip
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
procNetRouteErr.Set(true)
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
|
||||
}
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// Android apps don't have permission to read /proc/net/route, at
|
||||
// least on Google devices and the Android emulator.
|
||||
func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
line = line[len(pfx):]
|
||||
sp := bytes.IndexByte(line, ' ')
|
||||
if sp == -1 {
|
||||
return nil
|
||||
}
|
||||
ipb := line[:sp]
|
||||
if ip, err := netaddr.ParseIP(string(ipb)); err == nil && ip.Is4() {
|
||||
ret = ip
|
||||
log.Printf("interfaces: found Android default route %v", ip)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// DefaultRouteInterface returns the name of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces.
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
v, err := defaultRouteInterfaceProcNet()
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if runtime.GOOS == "android" {
|
||||
return defaultRouteInterfaceAndroidIPRoute()
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
|
||||
func defaultRouteInterfaceProcNet() (string, error) {
|
||||
f, err := os.Open("/proc/net/route")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
br := bufio.NewReaderSize(f, 128)
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Contains(line, zeroRouteBytes) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(string(line))
|
||||
ifc := fields[0]
|
||||
ip := fields[1]
|
||||
netmask := fields[7]
|
||||
|
||||
if strings.HasPrefix(ifc, "tailscale") ||
|
||||
strings.HasPrefix(ifc, "wg") {
|
||||
continue
|
||||
}
|
||||
if ip == "00000000" && netmask == "00000000" {
|
||||
// default route
|
||||
return ifc, nil // interface name
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no default routes found")
|
||||
|
||||
}
|
||||
|
||||
// defaultRouteInterfaceAndroidIPRoute tries to find the machine's default route interface name
|
||||
// by parsing the "ip route" command output. We use this on Android where /proc/net/route
|
||||
// can be missing entries or have locked-down permissions.
|
||||
// See also comments in https://github.com/tailscale/tailscale/pull/666.
|
||||
func defaultRouteInterfaceAndroidIPRoute() (ifname string, err error) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return "", err
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
ff := strings.Fields(string(line))
|
||||
for i, v := range ff {
|
||||
if i > 0 && ff[i-1] == "dev" && ifname == "" {
|
||||
ifname = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
if ifname == "" {
|
||||
return "", errors.New("no default routes found")
|
||||
}
|
||||
return ifname, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user