Compare commits
844 Commits
crawshaw/s
...
clone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6dbb4425c | ||
|
|
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 | ||
|
|
e3559d1c45 | ||
|
|
46f4b18fe8 | ||
|
|
3b94eabee3 | ||
|
|
44b07aa708 | ||
|
|
828aa6dcb0 | ||
|
|
495796fff1 | ||
|
|
108237798d | ||
|
|
68a173bc24 | ||
|
|
3b546dc29a | ||
|
|
fefd7e10dc | ||
|
|
7b901fdbbc | ||
|
|
0068e57407 | ||
|
|
9d1f48032a | ||
|
|
5650f1ecf9 | ||
|
|
c10f90357e | ||
|
|
657f331e8b | ||
|
|
9396024bd7 | ||
|
|
755fd9253c | ||
|
|
1ac570def7 | ||
|
|
ee3395e63a | ||
|
|
5e5e5db75f | ||
|
|
65fbb9c303 | ||
|
|
d6c34368e8 | ||
|
|
64db026c8b | ||
|
|
ced9b4008a | ||
|
|
d7429b9a8d | ||
|
|
85e675940d | ||
|
|
dbc1f71e5d | ||
|
|
2372530964 | ||
|
|
0038223632 | ||
|
|
e1526b796e | ||
|
|
d2b7cb1e45 | ||
|
|
f4c7eb8c44 | ||
|
|
18017f7630 | ||
|
|
66c7875974 | ||
|
|
cbb1e2e853 | ||
|
|
8296c934ac | ||
|
|
9669b85b41 | ||
|
|
ad0795efc2 | ||
|
|
a464439658 | ||
|
|
2244cca5ff | ||
|
|
58e83d8f66 | ||
|
|
172d72a060 | ||
|
|
5d67365cc9 | ||
|
|
9497921f52 | ||
|
|
c726c1eec9 | ||
|
|
1a0f6fea58 | ||
|
|
95a658e1e3 | ||
|
|
eb6de2bd88 | ||
|
|
6fcbd4c4d4 | ||
|
|
b6fa5a69be | ||
|
|
d427fc023e | ||
|
|
28c632c97b | ||
|
|
8fd8fc9c7d | ||
|
|
bfc1261ab6 | ||
|
|
f5993f2440 | ||
|
|
6b232b5a79 | ||
|
|
3bab226299 | ||
|
|
2dac4f2b24 | ||
|
|
eccae0cd0c | ||
|
|
0a50ecbad3 | ||
|
|
b8594dc937 | ||
|
|
d052586da7 | ||
|
|
108e3af76f | ||
|
|
268d331cb5 | ||
|
|
00d053e25a | ||
|
|
7fc97c5493 | ||
|
|
6fb30ff543 | ||
|
|
42e62a7eb2 | ||
|
|
ccfc4e0634 | ||
|
|
afbfe4f217 | ||
|
|
b925e18f70 | ||
|
|
dc9b39e3fb | ||
|
|
adf74c75ed | ||
|
|
3464114b88 | ||
|
|
780deb9996 | ||
|
|
614eec174f | ||
|
|
347a926ac4 | ||
|
|
45f2b53aca | ||
|
|
a7e7c7b548 | ||
|
|
614261d00d | ||
|
|
c0697e1feb | ||
|
|
a55c4217db | ||
|
|
3a3b64301e | ||
|
|
806645ea0e | ||
|
|
f2c2d0de68 | ||
|
|
39ae80a2e7 | ||
|
|
1ec27dbb5b | ||
|
|
b24029717b | ||
|
|
5b95aa81ea | ||
|
|
fe149979e6 | ||
|
|
73249f8032 | ||
|
|
922d9546bf | ||
|
|
6b2e29867e | ||
|
|
f8b72d2b5b | ||
|
|
1747d099e9 | ||
|
|
3f087466f5 | ||
|
|
22a0acff39 | ||
|
|
a279032998 | ||
|
|
0f64d7f9cc | ||
|
|
59800138ff | ||
|
|
277fe84c6b | ||
|
|
2776209e49 | ||
|
|
5bb14c07dc | ||
|
|
ec0cd60fc3 | ||
|
|
96b2f20c5b | ||
|
|
44434fdc82 | ||
|
|
e19287f60f | ||
|
|
71d6738333 | ||
|
|
febdac0499 | ||
|
|
02948d2c86 | ||
|
|
d503dee6f1 | ||
|
|
4e0ee141e8 | ||
|
|
996bf9cae7 | ||
|
|
7bf436ba30 | ||
|
|
64334143a1 | ||
|
|
bdc55d7091 | ||
|
|
50aeb5b9ad | ||
|
|
b446de103a | ||
|
|
af3a9dfad6 | ||
|
|
ba2774ea27 | ||
|
|
362d6af4e2 | ||
|
|
e71a7c7a2c | ||
|
|
fcb6a34f4b | ||
|
|
2863e49db9 | ||
|
|
3b4b17d239 | ||
|
|
5d995d9d6b | ||
|
|
2c2dff9559 | ||
|
|
2f8719741e | ||
|
|
48d7ee1c6a | ||
|
|
8ca796d144 | ||
|
|
c6aa5b639f | ||
|
|
4524dcf51e | ||
|
|
c14bc028ac | ||
|
|
1df3c8d02a | ||
|
|
5362e952e1 | ||
|
|
fee2d9fad4 | ||
|
|
0590ad68be | ||
|
|
a4ef345737 | ||
|
|
810c1e9704 | ||
|
|
f51f18b42b | ||
|
|
8ebee05fbd | ||
|
|
80261b02ba | ||
|
|
1fdadf06f1 | ||
|
|
8b3f6be008 | ||
|
|
322499473e | ||
|
|
dbca186a64 | ||
|
|
7fb8d873ac | ||
|
|
2d48f92a82 | ||
|
|
2c7ddd0828 | ||
|
|
577f321c38 | ||
|
|
cbd8aceb95 | ||
|
|
2e420ad8b6 | ||
|
|
6284454ae5 | ||
|
|
f53e78e0d5 | ||
|
|
4336de0d98 | ||
|
|
a56e853b72 | ||
|
|
73fccd8b06 | ||
|
|
d321190578 | ||
|
|
3c3ea8bc8a | ||
|
|
82ed7e527e | ||
|
|
8454bbbda5 | ||
|
|
64aff0db7b | ||
|
|
7740cbd8d9 | ||
|
|
680311b3df | ||
|
|
c473927558 | ||
|
|
e749377a56 | ||
|
|
1d3f45f9be | ||
|
|
ea9310403d | ||
|
|
1ab5b31c4b | ||
|
|
e60b433831 | ||
|
|
b6f77cc48d | ||
|
|
8c4cef60f8 | ||
|
|
521ad7b0fc | ||
|
|
0a25abcdaa | ||
|
|
8519a1b29f | ||
|
|
c34b350efa | ||
|
|
77921a31b1 | ||
|
|
1453aecb44 | ||
|
|
ea90780066 | ||
|
|
fd824df1fa | ||
|
|
64b5248929 | ||
|
|
58b8ebd290 | ||
|
|
1e031001db | ||
|
|
dbfc916273 | ||
|
|
dd31285ad4 | ||
|
|
af277a6762 | ||
|
|
221e7d7767 | ||
|
|
33bdcabf03 | ||
|
|
13eed1e88b | ||
|
|
f9cc4e8f53 | ||
|
|
0be475ba46 | ||
|
|
f9184ad12b | ||
|
|
94024355ed | ||
|
|
12a6626a94 | ||
|
|
2e43cd3f95 | ||
|
|
b1d5548fa7 | ||
|
|
51b669e4bd | ||
|
|
60ea635c6d | ||
|
|
6ddbcab71e | ||
|
|
51a12d1307 | ||
|
|
a184e05290 | ||
|
|
073bb8de80 | ||
|
|
7caa288213 | ||
|
|
5fa825f057 | ||
|
|
addbdce296 | ||
|
|
1ad78ce698 | ||
|
|
455ba751d9 | ||
|
|
73cae4eb11 | ||
|
|
acd14960f8 | ||
|
|
14a2564ef0 | ||
|
|
19a1704abd | ||
|
|
6de37f4cc0 | ||
|
|
df4636567f | ||
|
|
131541c06d | ||
|
|
52d9613b42 | ||
|
|
315a5e5355 | ||
|
|
3e2fadf872 | ||
|
|
98eceae55e | ||
|
|
f8d67bb591 | ||
|
|
c706731dc7 | ||
|
|
a567c56971 | ||
|
|
5a0822eefd | ||
|
|
e085aec8ef | ||
|
|
dd14b658a2 | ||
|
|
9ea445acd1 | ||
|
|
5758d8d48f | ||
|
|
bf98913106 | ||
|
|
8f9fa6a842 | ||
|
|
8de67844bd | ||
|
|
5aafe0ee96 | ||
|
|
d348b94505 | ||
|
|
1b2be3f1c8 | ||
|
|
8712164a0a | ||
|
|
290f83e9f6 | ||
|
|
7a57ab1793 | ||
|
|
ea0836dce9 | ||
|
|
120273d7f6 | ||
|
|
6c3820e8c4 | ||
|
|
19cc4f8b8e | ||
|
|
42e98d4edc | ||
|
|
db2436c7ff | ||
|
|
db31550854 | ||
|
|
7dd63abaed | ||
|
|
b23cb11eaf | ||
|
|
aeb88864e0 | ||
|
|
8b8e3f08a0 | ||
|
|
b4897e7de8 | ||
|
|
96bb05ce2f | ||
|
|
f2e2ffa423 | ||
|
|
b9c6d3ceb8 | ||
|
|
a87ee4168a | ||
|
|
bc73dcf204 | ||
|
|
8807913be9 | ||
|
|
eff6dcdb4e | ||
|
|
5ad947c761 | ||
|
|
72dbf26f63 | ||
|
|
e838b3fb59 | ||
|
|
3df1b97ea8 | ||
|
|
43aa8595dd | ||
|
|
41ac4a79d6 | ||
|
|
52c0cb12fb | ||
|
|
b4d02a251a | ||
|
|
57f220656c | ||
|
|
40c6f952c5 | ||
|
|
509247bf42 | ||
|
|
afc3479d04 | ||
|
|
b3ddf51a15 | ||
|
|
0d3f42e1d8 | ||
|
|
ed7e088729 | ||
|
|
4fd29349b9 | ||
|
|
b364a871bf | ||
|
|
72d9e1d633 | ||
|
|
b0f8931d26 | ||
|
|
7ec54e0064 | ||
|
|
af58cfc476 | ||
|
|
01b4bec33f | ||
|
|
4affea2691 | ||
|
|
77af7e5436 | ||
|
|
7eda3af034 | ||
|
|
d651715528 | ||
|
|
86baf60bd4 | ||
|
|
023df9239e | ||
|
|
592fec7606 | ||
|
|
a265d7cbff | ||
|
|
5c1e443d34 | ||
|
|
39c0ae1dba | ||
|
|
bd0e20f351 | ||
|
|
d44325295e | ||
|
|
d07146aafb | ||
|
|
5f2a17482b | ||
|
|
cac68fe102 | ||
|
|
5d8001d0ad | ||
|
|
8f0fd01efd | ||
|
|
5a1ce4adae | ||
|
|
4800926006 | ||
|
|
e201f63230 | ||
|
|
f5e0407e97 | ||
|
|
0f73070a57 | ||
|
|
d99498bee2 | ||
|
|
1dee36a984 | ||
|
|
66505358c0 | ||
|
|
ce7f6b2df1 | ||
|
|
8ae3ba0cf5 | ||
|
|
bb93d7aaba | ||
|
|
f42b9b6c9a | ||
|
|
96e0f86263 | ||
|
|
b9512edb7b | ||
|
|
e3172ae267 | ||
|
|
9b802d338c | ||
|
|
f265603110 | ||
|
|
77354d4617 | ||
|
|
fdee5fb639 | ||
|
|
f6dd2128d9 | ||
|
|
47265000da | ||
|
|
643bf14653 | ||
|
|
6b49347e4d | ||
|
|
bc20e555c5 | ||
|
|
a9cd8f31de | ||
|
|
890f5dff40 | ||
|
|
8abdbbdd1f | ||
|
|
905fe9f6a2 | ||
|
|
c8ebac2def | ||
|
|
cd1ac63b4c | ||
|
|
c5835c6ced | ||
|
|
946df89fa6 | ||
|
|
5562fae9f0 | ||
|
|
61d83f759b | ||
|
|
bd60a750e8 | ||
|
|
ef31dd7bb5 | ||
|
|
05a52746a4 | ||
|
|
97e58ad44d | ||
|
|
fbab12c94c | ||
|
|
fe0051fafd | ||
|
|
e733fa6f7e | ||
|
|
dd456f04c5 | ||
|
|
2839854994 | ||
|
|
4f5c0da1ae | ||
|
|
61f3fda405 | ||
|
|
6978b93bdd | ||
|
|
12b77f30ad | ||
|
|
cf4aacde57 | ||
|
|
2cff9016e4 | ||
|
|
a36ccb8525 | ||
|
|
8f9849c140 | ||
|
|
7e1bed82bd | ||
|
|
40ebba1373 | ||
|
|
848a2bddf0 | ||
|
|
551964c562 | ||
|
|
4a7638c319 | ||
|
|
7932481b95 | ||
|
|
57de94c7aa | ||
|
|
d580157921 | ||
|
|
ff6b3c2c38 | ||
|
|
f192c05413 | ||
|
|
eac62ec5ff | ||
|
|
bf704a5218 | ||
|
|
a65b2a0efd | ||
|
|
a33419167b | ||
|
|
9be9738f62 | ||
|
|
caec2c7e8b | ||
|
|
9f584414d9 | ||
|
|
34859f8e7d | ||
|
|
75e62d318f | ||
|
|
596fd449b9 | ||
|
|
b27d4c017a | ||
|
|
4cf5ac3060 | ||
|
|
724c37fb41 | ||
|
|
dbc99dc0d2 | ||
|
|
20da44eae3 | ||
|
|
e371520cc5 | ||
|
|
89a2c3eb04 | ||
|
|
21fc5ec371 | ||
|
|
dd68debd64 | ||
|
|
68ff31a0ba | ||
|
|
b3d9eab1fe | ||
|
|
383d86df5f | ||
|
|
65e7c58aa4 | ||
|
|
06092a3af3 | ||
|
|
ef1f967ec0 | ||
|
|
844d991baf | ||
|
|
657f9593ae | ||
|
|
61529ac459 | ||
|
|
ca11e3804b | ||
|
|
4675c70464 | ||
|
|
7172f3dbf4 | ||
|
|
11489a6e67 | ||
|
|
bc7bc43fb8 | ||
|
|
af7a01d6f0 | ||
|
|
a399ef3dc7 | ||
|
|
051b6ef141 | ||
|
|
cc4afa775f | ||
|
|
9cdaeac00a | ||
|
|
92bae83cc1 | ||
|
|
719c974c38 | ||
|
|
0752c77dc2 | ||
|
|
c6550135d5 | ||
|
|
18fb98792b | ||
|
|
061315acb4 | ||
|
|
1dbd1babc9 | ||
|
|
d5050c37a5 | ||
|
|
d417be6a4b | ||
|
|
8aa2090919 | ||
|
|
d133339216 | ||
|
|
bcf3719b9e | ||
|
|
1abf2da392 | ||
|
|
517a91de75 | ||
|
|
d59d0576af | ||
|
|
38acccad09 | ||
|
|
752146a70f | ||
|
|
cdc10b74f1 | ||
|
|
3425d8d84f | ||
|
|
e7cdc11654 | ||
|
|
67ede8d6d2 | ||
|
|
4cd3e82bbd | ||
|
|
a6c695ba6b | ||
|
|
3878f0a67a | ||
|
|
07f0fd0fdc | ||
|
|
f3c0ea23d9 | ||
|
|
19b54d0ae7 | ||
|
|
c576a57067 | ||
|
|
06bf0d980e | ||
|
|
78654ee1bd | ||
|
|
4e3ff4b63b | ||
|
|
25797c8c2a | ||
|
|
14559340ee | ||
|
|
a07af762e4 | ||
|
|
c185e6b4b0 | ||
|
|
00ad93ec25 | ||
|
|
0e128f8f22 | ||
|
|
74650d6fb5 | ||
|
|
14abc82033 | ||
|
|
2489ea4268 | ||
|
|
6879581891 | ||
|
|
eefafad9f8 | ||
|
|
6d2ac01464 | ||
|
|
44670d0da9 | ||
|
|
9390a3ef55 | ||
|
|
7a3be96199 | ||
|
|
3988ddc85d | ||
|
|
758744a4e3 | ||
|
|
d8de11a01b | ||
|
|
824f825552 | ||
|
|
367ffde21a | ||
|
|
04e6b77774 | ||
|
|
29f7d64091 |
6
.github/workflows/cross-darwin.yml
vendored
6
.github/workflows/cross-darwin.yml
vendored
@@ -3,7 +3,7 @@ name: Darwin-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
6
.github/workflows/cross-freebsd.yml
vendored
6
.github/workflows/cross-freebsd.yml
vendored
@@ -3,7 +3,7 @@ name: FreeBSD-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
6
.github/workflows/cross-openbsd.yml
vendored
6
.github/workflows/cross-openbsd.yml
vendored
@@ -3,7 +3,7 @@ name: OpenBSD-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
6
.github/workflows/cross-windows.yml
vendored
6
.github/workflows/cross-windows.yml
vendored
@@ -3,7 +3,7 @@ name: Windows-Cross
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
40
.github/workflows/license.yml
vendored
Normal file
40
.github/workflows/license.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: license
|
||||
|
||||
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.14
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run license checker
|
||||
run: ./scripts/check_license_headers.sh .
|
||||
|
||||
- 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'
|
||||
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -3,7 +3,7 @@ name: Linux
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
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.14
|
||||
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'
|
||||
|
||||
14
.github/workflows/staticcheck.yml
vendored
14
.github/workflows/staticcheck.yml
vendored
@@ -3,7 +3,7 @@ name: staticcheck
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@@ -13,16 +13,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
go-version: 1.14
|
||||
|
||||
- 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
|
||||
|
||||
- name: Run staticcheck
|
||||
run: go run honnef.co/go/tools/cmd/staticcheck -- ./...
|
||||
run: "go run honnef.co/go/tools/cmd/staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
23
Dockerfile
23
Dockerfile
@@ -2,7 +2,26 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
FROM golang:1.13-alpine AS build-env
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
#
|
||||
# $ docker build -t tailscale:tailscale .
|
||||
#
|
||||
# To run the tailscaled agent:
|
||||
#
|
||||
# $ docker run -d --name=tailscaled -v /var/lib:/var/lib -v /dev/net/tun:/dev/net/tun --network=host --privileged tailscale:tailscale tailscaled
|
||||
#
|
||||
# To then log in:
|
||||
#
|
||||
# $ docker exec tailscaled tailscale up
|
||||
#
|
||||
# To see status:
|
||||
#
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.14-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -15,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/
|
||||
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
check: staticcheck
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
13
README.md
13
README.md
@@ -12,16 +12,21 @@ It currently includes the Linux client.
|
||||
The Linux client is currently `cmd/relaynode`, but will
|
||||
soon be replaced by `cmd/tailscaled`.
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
https://pkgs.tailscale.com .
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
|
||||
We only support the latest Go release and any Go beta or release
|
||||
candidate builds (currently Go 1.13.x or Go 1.14) in module mode. It
|
||||
might work in earlier Go versions or in GOPATH mode, but we're making
|
||||
no effort to keep those working.
|
||||
We 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
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
## Bugs
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 Tailscale & AUTHORS. All rights reserved.
|
||||
// Copyright (c) 2019 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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
264
cmd/cloner/cloner.go
Normal file
264
cmd/cloner/cloner.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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")
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
_ = t
|
||||
name := typ.Obj().Name()
|
||||
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
|
||||
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
|
||||
writef := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(buf, "\t"+format+"\n", args...)
|
||||
}
|
||||
writef("if src == nil {")
|
||||
writef("\treturn nil")
|
||||
writef("}")
|
||||
writef("dst := new(%s)", name)
|
||||
writef("*dst = *src")
|
||||
for i := 0; i < t.NumFields(); i++ {
|
||||
fname := t.Field(i).Name()
|
||||
ft := t.Field(i).Type()
|
||||
if !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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
_ "expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,9 +17,9 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,8 +28,10 @@ import (
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/interfaces"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -36,20 +39,14 @@ var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certDir = flag.String("certdir", defaultCertDir(), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
mbps = flag.Int("mbps", 5, "Mbps (mebibit/s) per-client rate limit; 0 means unlimited")
|
||||
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")
|
||||
)
|
||||
|
||||
func defaultCertDir() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err == nil {
|
||||
return filepath.Join(cacheDir, "tailscale", "derper-certs")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
}
|
||||
@@ -110,6 +107,7 @@ func main() {
|
||||
*logCollection = ""
|
||||
*addr = ":3340" // above the keys DERP
|
||||
log.Printf("Running in dev mode.")
|
||||
tsweb.DevMode = true
|
||||
}
|
||||
|
||||
var logPol *logpolicy.Policy
|
||||
@@ -120,24 +118,30 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
letsEncrypt := false
|
||||
if _, port, _ := net.SplitHostPort(*addr); port == "443" {
|
||||
letsEncrypt = true
|
||||
}
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
if *mbps != 0 {
|
||||
s.BytesPerSecond = (*mbps << 20) / 8
|
||||
|
||||
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())
|
||||
expvar.Publish("uptime", uptimeVar{})
|
||||
|
||||
// Create our own mux so we don't expose /debug/ stuff to the world.
|
||||
mux := http.NewServeMux()
|
||||
mux := tsweb.NewMux(debugHandler(s))
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
mux.Handle("/debug/", protected(debugHandler(s)))
|
||||
mux.Handle("/debug/pprof/", protected(http.DefaultServeMux)) // to net/http/pprof
|
||||
mux.Handle("/debug/vars", protected(http.DefaultServeMux)) // to expvar
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -150,11 +154,15 @@ func main() {
|
||||
server.
|
||||
</p>
|
||||
`)
|
||||
if allowDebugAccess(r) {
|
||||
if tsweb.AllowDebugAccess(r) {
|
||||
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
|
||||
}
|
||||
}))
|
||||
|
||||
if *runSTUN {
|
||||
go serveSTUN()
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
@@ -171,9 +179,13 @@ func main() {
|
||||
HostPolicy: autocert.HostWhitelist(*hostname),
|
||||
Cache: autocert.DirCache(*certDir),
|
||||
}
|
||||
if *hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
go func() {
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(port80Handler{mux}))
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
@@ -190,82 +202,120 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func protected(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowDebugAccess(r) {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func allowDebugAccess(r *http.Request) bool {
|
||||
if r.Header.Get("X-Forwarded-For") != "" {
|
||||
// TODO if/when needed. For now, conservative:
|
||||
return false
|
||||
}
|
||||
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
return interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("ALLOW_DEBUG_IP")
|
||||
}
|
||||
|
||||
type port80Handler struct{ tlsHandler http.Handler }
|
||||
|
||||
func (h port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.RequestURI
|
||||
if path == "/debug" || strings.HasPrefix(path, "/debug") {
|
||||
h.tlsHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
http.Error(w, "Use HTTPS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if path == "/" && allowDebugAccess(r) {
|
||||
// Redirect authorized user to the debug handler.
|
||||
path = "/debug/"
|
||||
}
|
||||
target := "https://" + stripPort(r.Host) + path
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
||||
|
||||
func stripPort(hostport string) string {
|
||||
host, _, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
return net.JoinHostPort(host, "443")
|
||||
}
|
||||
|
||||
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>Rate Limit:</b> %v Mbps</li>\n", *mbps)
|
||||
f("<li><b>Uptime:</b> %v</li>\n", uptime().Round(time.Second))
|
||||
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
|
||||
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
|
||||
|
||||
f(`<li><a href="/debug/vars">/debug/vars</a></li>
|
||||
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>
|
||||
`)
|
||||
})
|
||||
}
|
||||
|
||||
var timeStart = time.Now()
|
||||
func serveSTUN() {
|
||||
pc, err := net.ListenPacket("udp", ":3478")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open STUN listener: %v", err)
|
||||
}
|
||||
log.Printf("running STUN server on %v", pc.LocalAddr())
|
||||
|
||||
func uptime() time.Duration { return time.Since(timeStart) }
|
||||
var (
|
||||
stats = new(metrics.Set)
|
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"}
|
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"}
|
||||
|
||||
type uptimeVar struct{}
|
||||
stunReadError = stunDisposition.Get("read_error")
|
||||
stunNotSTUN = stunDisposition.Get("not_stun")
|
||||
stunWriteError = stunDisposition.Get("write_error")
|
||||
stunSuccess = stunDisposition.Get("success")
|
||||
|
||||
func (uptimeVar) String() string { return fmt.Sprint(int64(uptime().Seconds())) }
|
||||
stunIPv4 = stunAddrFamily.Get("ipv4")
|
||||
stunIPv6 = stunAddrFamily.Get("ipv6")
|
||||
)
|
||||
stats.Set("counter_requests", stunDisposition)
|
||||
stats.Set("counter_addrfamily", stunAddrFamily)
|
||||
expvar.Publish("stun", stats)
|
||||
|
||||
var buf [64 << 10]byte
|
||||
for {
|
||||
n, addr, err := pc.ReadFrom(buf[:])
|
||||
if err != nil {
|
||||
log.Printf("STUN ReadFrom: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
stunReadError.Add(1)
|
||||
continue
|
||||
}
|
||||
ua, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Printf("STUN unexpected address %T %v", addr, addr)
|
||||
stunReadError.Add(1)
|
||||
continue
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if !stun.Is(pkt) {
|
||||
stunNotSTUN.Add(1)
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(pkt)
|
||||
if err != nil {
|
||||
stunNotSTUN.Add(1)
|
||||
continue
|
||||
}
|
||||
if ua.IP.To4() != nil {
|
||||
stunIPv4.Add(1)
|
||||
} else {
|
||||
stunIPv6.Add(1)
|
||||
}
|
||||
res := stun.Response(txid, ua.IP, uint16(ua.Port))
|
||||
_, err = pc.WriteTo(res, addr)
|
||||
if err != nil {
|
||||
stunWriteError.Add(1)
|
||||
} else {
|
||||
stunSuccess.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
|
||||
|
||||
func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
if validProdHostname.MatchString(host) {
|
||||
return nil
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
|
||||
35
cmd/derper/derper_test.go
Normal file
35
cmd/derper/derper_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
wantOK bool
|
||||
}{
|
||||
{"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", true},
|
||||
{"derp02.bar.tailscale.com", false},
|
||||
{"example.net", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := prodAutocertHostPolicy(context.Background(), tt.in) == nil
|
||||
if got != tt.wantOK {
|
||||
t.Errorf("f(%q) = %v; want %v", tt.in, got, tt.wantOK)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
175
cmd/microproxy/microproxy.go
Normal file
175
cmd/microproxy/microproxy.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.
|
||||
|
||||
// microproxy proxies incoming HTTPS connections to another
|
||||
// destination. Instead of managing its own TLS certificates, it
|
||||
// borrows issued certificates and keys from an autocert directory.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":4430", "server address")
|
||||
certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
|
||||
hostname = flag.String("hostname", "", "hostname to serve")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
|
||||
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *logCollection != "" {
|
||||
logpolicy.New(*logCollection)
|
||||
}
|
||||
|
||||
ne, err := url.Parse(*nodeExporter)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(ne)
|
||||
proxy.FlushInterval = time.Second
|
||||
|
||||
if _, err = url.Parse(*goVarsURL); err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
|
||||
}
|
||||
|
||||
mux := tsweb.NewMux(http.HandlerFunc(debugHandler))
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, log.Printf)))
|
||||
|
||||
ch := &certHolder{
|
||||
hostname: *hostname,
|
||||
path: filepath.Join(*certdir, *hostname),
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: ch.GetCertificate,
|
||||
},
|
||||
}
|
||||
|
||||
if err := httpsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type goVarsHandler struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
for k, i := range obj {
|
||||
if prefix != "" {
|
||||
k = prefix + "_" + k
|
||||
}
|
||||
switch v := i.(type) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
resp, err := http.Get(h.url)
|
||||
if err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var mon map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
promPrint(w, "", mon)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certHolder loads and caches a TLS certificate from disk, reloading
|
||||
// it every hour.
|
||||
type certHolder struct {
|
||||
hostname string // only hostname allowed in SNI
|
||||
path string // path of certificate+key combined PEM file
|
||||
|
||||
mu sync.Mutex
|
||||
cert *tls.Certificate // cached parsed cert+key
|
||||
loaded time.Time
|
||||
}
|
||||
|
||||
func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ch.ServerName != c.hostname {
|
||||
return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if time.Since(c.loaded) > time.Hour {
|
||||
if err := c.loadLocked(); err != nil {
|
||||
log.Printf("Reloading cert %q: %v", c.path, err)
|
||||
// continue anyway, we might be able to serve off the stale cert.
|
||||
}
|
||||
}
|
||||
return c.cert, nil
|
||||
}
|
||||
|
||||
// load reloads the TLS certificate and key from disk. Caller must
|
||||
// hold mu.
|
||||
func (c *certHolder) loadLocked() error {
|
||||
bs, err := ioutil.ReadFile(c.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %v", c.path, err)
|
||||
}
|
||||
cert, err := tls.X509KeyPair(bs, bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %q: %v", c.path, err)
|
||||
}
|
||||
|
||||
c.cert = &cert
|
||||
c.loaded = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// debugHandler serves a page with links to tsweb-managed debug URLs
|
||||
// at /debug/.
|
||||
func debugHandler(w http.ResponseWriter, r *http.Request) {
|
||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
||||
f(`<html><body>
|
||||
<h1>microproxy debug</h1>
|
||||
<ul>
|
||||
`)
|
||||
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
|
||||
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
|
||||
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>
|
||||
<ul>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
@@ -31,16 +31,28 @@ func parseFiles(s string) (map[string]string, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parseEmptyDirs(s string) []string {
|
||||
// strings.Split("", ",") would return []string{""}, which is not suitable:
|
||||
// this would create an empty dir record with path "", breaking the package
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := getopt.StringLong("out", 'o', "", "output file to write")
|
||||
goarch := getopt.StringLong("arch", 'a', "amd64", "GOARCH this package is for")
|
||||
pkgType := getopt.StringLong("type", 't', "deb", "type of package to build (deb or rpm)")
|
||||
files := getopt.StringLong("files", 'F', "", "comma-separated list of files in src:dst form")
|
||||
configFiles := getopt.StringLong("configs", 'C', "", "like --files, but for files marked as user-editable config files")
|
||||
emptyDirs := getopt.StringLong("emptydirs", 'E', "", "comma-separated list of empty directories")
|
||||
version := getopt.StringLong("version", 0, "0.0.0", "version of the package")
|
||||
postinst := getopt.StringLong("postinst", 0, "", "debian postinst script path")
|
||||
prerm := getopt.StringLong("prerm", 0, "", "debian prerm script path")
|
||||
postrm := getopt.StringLong("postrm", 0, "", "debian postrm script path")
|
||||
replaces := getopt.StringLong("replaces", 0, "", "package which this package replaces, if any")
|
||||
depends := getopt.StringLong("depends", 0, "", "comma-separated list of packages this package depends on")
|
||||
getopt.Parse()
|
||||
|
||||
filesMap, err := parseFiles(*files)
|
||||
@@ -51,6 +63,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("Parsing --configs: %v", err)
|
||||
}
|
||||
emptyDirList := parseEmptyDirs(*emptyDirs)
|
||||
info := nfpm.WithDefaults(&nfpm.Info{
|
||||
Name: "tailscale",
|
||||
Arch: *goarch,
|
||||
@@ -61,20 +74,29 @@ func main() {
|
||||
Homepage: "https://www.tailscale.com",
|
||||
License: "MIT",
|
||||
Overridables: nfpm.Overridables{
|
||||
Files: filesMap,
|
||||
ConfigFiles: configsMap,
|
||||
EmptyFolders: emptyDirList,
|
||||
Files: filesMap,
|
||||
ConfigFiles: configsMap,
|
||||
Scripts: nfpm.Scripts{
|
||||
PostInstall: *postinst,
|
||||
PreRemove: *prerm,
|
||||
PostRemove: *postrm,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if len(*depends) != 0 {
|
||||
info.Overridables.Depends = strings.Split(*depends, ",")
|
||||
}
|
||||
if *replaces != "" {
|
||||
info.Overridables.Replaces = []string{*replaces}
|
||||
info.Overridables.Conflicts = []string{*replaces}
|
||||
}
|
||||
|
||||
switch *pkgType {
|
||||
case "deb":
|
||||
info.Section = "net"
|
||||
info.Priority = "extra"
|
||||
info.Overridables.Scripts = nfpm.Scripts{
|
||||
PostInstall: *postinst,
|
||||
PreRemove: *prerm,
|
||||
PostRemove: *postrm,
|
||||
}
|
||||
case "rpm":
|
||||
info.Overridables.RPM.Group = "Network"
|
||||
}
|
||||
|
||||
14
cmd/relaynode/.gitignore
vendored
14
cmd/relaynode/.gitignore
vendored
@@ -1,14 +0,0 @@
|
||||
/*.tar.gz
|
||||
/*.deb
|
||||
/*.rpm
|
||||
/*.spec
|
||||
/pkgver
|
||||
debian/changelog
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/*.log
|
||||
debian/*.substvars
|
||||
debian/*.debhelper
|
||||
debian/tailscale-relay
|
||||
/tailscale-relay/
|
||||
/tailscale-relay-*
|
||||
@@ -1 +0,0 @@
|
||||
rm -f debian/changelog *~ debian/*~
|
||||
@@ -1,13 +0,0 @@
|
||||
exec >&2
|
||||
read -r package <package
|
||||
rm -f *~ .*~ \
|
||||
debian/*~ debian/changelog debian/debhelper-build-stamp \
|
||||
debian/*.log debian/files debian/*.substvars debian/*.debhelper \
|
||||
*.tar.gz *.deb *.rpm *.spec pkgver relaynode *.exe
|
||||
[ -n "$package" ] && rm -rf "debian/$package"
|
||||
for d in */.stamp; do
|
||||
if [ -e "$d" ]; then
|
||||
dir=$(dirname "$d")
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done
|
||||
@@ -1,10 +0,0 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
arch=$(dpkg --print-architecture)
|
||||
|
||||
redo-ifchange "$dir/${package}_$arch.deb"
|
||||
rm -f "$dir/${package}"_*_"$arch.deb"
|
||||
ln -sf "${package}_$arch.deb" "$dir/${package}_${version}_$arch.deb"
|
||||
@@ -1 +0,0 @@
|
||||
Tailscale IPN relay daemon.
|
||||
@@ -1,5 +0,0 @@
|
||||
redo-ifchange ../../../version/short.txt gen-changelog
|
||||
(
|
||||
cd ..
|
||||
debian/gen-changelog
|
||||
) >$3
|
||||
@@ -1 +0,0 @@
|
||||
9
|
||||
@@ -1,14 +0,0 @@
|
||||
Source: tailscale-relay
|
||||
Section: net
|
||||
Priority: extra
|
||||
Maintainer: Avery Pennarun <apenwarr@tailscale.com>
|
||||
Build-Depends: debhelper (>= 10.2.5), dh-systemd (>= 1.5)
|
||||
Standards-Version: 3.9.2
|
||||
Homepage: https://tailscale.com/
|
||||
Vcs-Git: https://github.com/tailscale/tailscale
|
||||
Vcs-Browser: https://github.com/tailscale/tailscale
|
||||
|
||||
Package: tailscale-relay
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Description: Traffic relay node for Tailscale IPN
|
||||
@@ -1,11 +0,0 @@
|
||||
Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
|
||||
Upstream-Name: tailscale-relay
|
||||
Upstream-Contact: Avery Pennarun <apenwarr@tailscale.com>
|
||||
Source: https://github.com/tailscale/tailscale/
|
||||
|
||||
Files: *
|
||||
Copyright: © 2019 Tailscale Inc. <info@tailscale.com>
|
||||
License: Proprietary
|
||||
*
|
||||
* Copyright 2019 Tailscale Inc. All rights reserved.
|
||||
*
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
read junk pkgname <debian/control
|
||||
read shortver <../../version/short.txt
|
||||
git log --pretty='format:'"$pkgname"' (SHA:%H) unstable; urgency=low
|
||||
|
||||
* %s
|
||||
|
||||
-- %aN <%aE> %aD
|
||||
' . |
|
||||
python -Sc '
|
||||
import os, re, subprocess, sys
|
||||
|
||||
first = True
|
||||
def Describe(g):
|
||||
global first
|
||||
if first:
|
||||
s = sys.argv[1]
|
||||
first = False
|
||||
else:
|
||||
sha = g.group(1)
|
||||
s = subprocess.check_output(["git", "describe", "--always", "--", sha]).strip().decode("utf-8")
|
||||
return re.sub(r"^\D*", "", s)
|
||||
|
||||
print(re.sub(r"SHA:([0-9a-f]+)", Describe, sys.stdin.read()))
|
||||
' "$shortver"
|
||||
@@ -1,3 +0,0 @@
|
||||
relaynode /usr/sbin
|
||||
tailscale-login /usr/sbin
|
||||
taillogin /usr/sbin
|
||||
@@ -1,8 +0,0 @@
|
||||
#DEBHELPER#
|
||||
|
||||
f=/var/lib/tailscale/relay.conf
|
||||
if ! [ -e "$f" ]; then
|
||||
echo
|
||||
echo "Note: Run tailscale-login to configure $f." >&2
|
||||
echo
|
||||
fi
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
DESTDIR=debian/tailscale-relay
|
||||
|
||||
override_dh_auto_test:
|
||||
override_dh_auto_install:
|
||||
mkdir -p "${DESTDIR}/etc/default"
|
||||
cp tailscale-relay.defaults "${DESTDIR}/etc/default/tailscale-relay"
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Traffic relay node for Tailscale IPN
|
||||
After=network.target
|
||||
ConditionPathExists=/var/lib/tailscale/relay.conf
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscale-relay
|
||||
ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $FLAGS
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,21 +0,0 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/oss/version/short.txt" "$S/$dir/package" "$dir/debtmp.dir"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
arch=$(dpkg --print-architecture)
|
||||
|
||||
(
|
||||
cd "$S/$dir"
|
||||
git ls-files debian | xargs redo-ifchange debian/changelog
|
||||
)
|
||||
cp -a "$S/$dir/debian" "$dir/debtmp/"
|
||||
rm -f "$dir/debtmp/debian/$package.debhelper.log"
|
||||
rm -f "$dir/${package}_${version}_${arch}.deb"
|
||||
(
|
||||
cd "$dir/debtmp" &&
|
||||
debian/rules build &&
|
||||
fakeroot debian/rules binary
|
||||
)
|
||||
|
||||
mv "$dir/${package}_${version}_${arch}.deb" "$3"
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generate a directory tree suitable for forming a tarball of
|
||||
# this package.
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
outdir=$PWD/${1%.dir}
|
||||
rm -rf "$outdir"
|
||||
mkdir "$outdir"
|
||||
touch $outdir/.stamp
|
||||
sfiles="
|
||||
tailscale-login
|
||||
debian/*.service
|
||||
*.defaults
|
||||
"
|
||||
ofiles="
|
||||
relaynode
|
||||
../taillogin/taillogin
|
||||
"
|
||||
redo-ifchange "$outdir/.stamp"
|
||||
(cd "$S/$dir" && redo-ifchange $sfiles && cp $sfiles "$outdir/")
|
||||
(cd "$dir" && redo-ifchange $ofiles && cp $ofiles "$outdir/")
|
||||
@@ -1,15 +0,0 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
pkg=${1##*/}
|
||||
pkg=${pkg%.rpm}
|
||||
redo-ifchange "$S/oss/version/short.txt" "$dir/$pkg.tar.gz" "$dir/$pkg.spec"
|
||||
read -r pkgver junk <"$S/oss/version/short.txt"
|
||||
|
||||
machine=$(uname -m)
|
||||
rpmbase=$HOME/rpmbuild
|
||||
|
||||
mkdir -p "$rpmbase/SOURCES/"
|
||||
cp "$dir/$pkg.tar.gz" "$rpmbase/SOURCES/"
|
||||
rm -f "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm"
|
||||
rpmbuild -bb "$dir/$pkg.spec"
|
||||
mv "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm" $3
|
||||
@@ -1,7 +0,0 @@
|
||||
redo-ifchange "$S/$1.in" "$S/oss/version/short.txt"
|
||||
read -r pkgver junk <"$S/oss/version/short.txt"
|
||||
basever=${pkgver%-*}
|
||||
subver=${pkgver#*-}
|
||||
sed -e "s/Version: 0.00$/Version: $basever/" \
|
||||
-e "s/Release: 0$/Release: $subver/" \
|
||||
<"$S/$1.in" >"$3"
|
||||
@@ -1,8 +0,0 @@
|
||||
exec >&2
|
||||
xdir=${1%.tar.gz}
|
||||
base=${xdir##*/}
|
||||
updir=${xdir%/*}
|
||||
redo-ifchange "$xdir.dir"
|
||||
OUT="$PWD/$3"
|
||||
|
||||
cd "$updir" && tar -czvf "$OUT" --exclude "$base/.stamp" "$base"
|
||||
@@ -1,15 +0,0 @@
|
||||
# Build packages for customer distribution.
|
||||
dir=${1%/*}
|
||||
cd "$dir"
|
||||
targets="tarball"
|
||||
if which dh_clean fakeroot dpkg >/dev/null; then
|
||||
targets="$targets deb"
|
||||
else
|
||||
echo "Skipping debian packages: debhelper and/or dpkg build tools missing." >&2
|
||||
fi
|
||||
if which rpm >/dev/null; then
|
||||
targets="$targets rpm"
|
||||
else
|
||||
echo "Skipping rpm packages: rpm build tools missing." >&2
|
||||
fi
|
||||
redo-ifchange $targets
|
||||
1
cmd/relaynode/docker/.gitignore
vendored
1
cmd/relaynode/docker/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/relaynode
|
||||
@@ -1,17 +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 with: docker build -t tailcontrol-alpine .
|
||||
# Run with: docker run --cap-add=NET_ADMIN --device=/dev/net/tun:/dev/net/tun -it tailcontrol-alpine
|
||||
|
||||
FROM debian:stretch-slim
|
||||
|
||||
RUN apt-get update && apt-get -y install iproute2 iptables
|
||||
RUN apt-get -y install ca-certificates
|
||||
RUN apt-get -y install nginx-light
|
||||
|
||||
COPY relaynode /
|
||||
|
||||
# tailcontrol -tun=wg0 -dbdir=$HOME/taildb >> tailcontrol.log 2>&1 &
|
||||
CMD ["/relaynode", "-R", "--config", "relay.conf"]
|
||||
@@ -1 +0,0 @@
|
||||
redo-ifchange build
|
||||
@@ -1,3 +0,0 @@
|
||||
exec >&2
|
||||
redo-ifchange Dockerfile relaynode
|
||||
docker build -t tailscale .
|
||||
@@ -1,2 +0,0 @@
|
||||
redo-ifchange ../relaynode
|
||||
cp ../relaynode $3
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
redo-ifchange build
|
||||
docker run --cap-add=NET_ADMIN \
|
||||
--device=/dev/net/tun:/dev/net/tun \
|
||||
-it tailscale
|
||||
@@ -1 +0,0 @@
|
||||
tailscale-relay
|
||||
@@ -1,239 +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.
|
||||
|
||||
// Relaynode is the old Linux Tailscale daemon.
|
||||
//
|
||||
// Deprecated: this program will be soon deleted. The replacement is
|
||||
// cmd/tailscaled.
|
||||
package main // import "tailscale.com/cmd/relaynode"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
config := getopt.StringLong("config", 'f', "", "path to config file")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
|
||||
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
|
||||
tunname := getopt.StringLong("tun", 0, "wg0", "tunnel interface name")
|
||||
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
|
||||
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
|
||||
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
|
||||
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
|
||||
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
|
||||
routes := getopt.StringLong("routes", 0, "", "list of IP ranges this node can relay")
|
||||
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
}
|
||||
uflags := controlclient.UFlagsHelper(!*nuroutes, *rroutes, *droutes)
|
||||
if *config == "" {
|
||||
log.Fatal("no --config file specified")
|
||||
}
|
||||
if *tunname == "" {
|
||||
log.Printf("Warning: no --tun device specified; routing disabled.\n")
|
||||
}
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
|
||||
// The wgengine takes a wireguard configuration produced by the
|
||||
// controlclient, and runs the actual tunnels and packets.
|
||||
var e wgengine.Engine
|
||||
if *fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, *listenport)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting wireguard engine: %v\n", err)
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
// Default filter blocks everything, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone())
|
||||
|
||||
var lastNetMap *controlclient.NetworkMap
|
||||
statusFunc := func(new controlclient.Status) {
|
||||
if new.URL != "" {
|
||||
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
|
||||
return
|
||||
}
|
||||
if new.Err != "" {
|
||||
log.Print(new.Err)
|
||||
return
|
||||
}
|
||||
if new.Persist != nil {
|
||||
if err := saveConfig(*config, *new.Persist); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
if m := new.NetMap; m != nil {
|
||||
if lastNetMap != nil {
|
||||
s1 := strings.Split(lastNetMap.Concise(), "\n")
|
||||
s2 := strings.Split(new.NetMap.Concise(), "\n")
|
||||
logf("netmap diff:\n%v\n", cmp.Diff(s1, s2))
|
||||
}
|
||||
lastNetMap = m
|
||||
|
||||
if m.Equal(&controlclient.NetworkMap{}) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("packet filter: %v\n", m.PacketFilter)
|
||||
e.SetFilter(filter.New(m.PacketFilter))
|
||||
|
||||
wgcfg, err := m.WGCfg(uflags, m.DNS)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting wg config: %v\n", err)
|
||||
}
|
||||
err = e.Reconfig(wgcfg, m.DNSDomains)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reconfiguring engine: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(*config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hi := controlclient.NewHostinfo()
|
||||
hi.FrontendLogID = pol.PublicID.String()
|
||||
hi.BackendLogID = pol.PublicID.String()
|
||||
if *routes != "" {
|
||||
for _, routeStr := range strings.Split(*routes, ",") {
|
||||
cidr, err := wgcfg.ParseCIDR(routeStr)
|
||||
if err != nil {
|
||||
log.Fatalf("--routes: not an IP range: %s", routeStr)
|
||||
}
|
||||
hi.RoutableIPs = append(hi.RoutableIPs, *cidr)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := controlclient.New(controlclient.Options{
|
||||
Persist: cfg,
|
||||
ServerURL: *server,
|
||||
Hostinfo: &hi,
|
||||
NewDecompressor: func() (controlclient.Decompressor, error) {
|
||||
return zstd.NewReader(nil)
|
||||
},
|
||||
KeepAlive: true,
|
||||
})
|
||||
c.SetStatusFunc(statusFunc)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lf := controlclient.LoginDefault
|
||||
if *alwaysrefresh {
|
||||
lf |= controlclient.LoginInteractive
|
||||
}
|
||||
c.Login(nil, lf)
|
||||
|
||||
// Print the wireguard status when we get an update.
|
||||
e.SetStatusCallback(func(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("Wireguard engine status error: %v\n", err)
|
||||
}
|
||||
var ss []string
|
||||
for _, p := range s.Peers {
|
||||
if p.LastHandshake.IsZero() {
|
||||
ss = append(ss, "x")
|
||||
} else {
|
||||
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
|
||||
}
|
||||
}
|
||||
logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
|
||||
c.UpdateEndpoints(0, s.LocalAddrs)
|
||||
})
|
||||
|
||||
if *debug != "" {
|
||||
go runDebugServer(*debug)
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
signal.Notify(sigCh, syscall.SIGTERM)
|
||||
|
||||
<-sigCh
|
||||
logf("signal received, exiting")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
e.Close()
|
||||
pol.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (cfg controlclient.Persist, err error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("config %s does not exist", path)
|
||||
return controlclient.Persist{}, nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(path string, cfg controlclient.Persist) error {
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDebugServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
1
cmd/relaynode/relaynode.od
Normal file
1
cmd/relaynode/relaynode.od
Normal file
@@ -0,0 +1 @@
|
||||
# placeholder to work around redo bug
|
||||
@@ -1,9 +0,0 @@
|
||||
exec >&2
|
||||
dir=${2%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r pkgver <"$S/oss/version/short.txt"
|
||||
machine=$(uname -m)
|
||||
redo-ifchange "$dir/$package.rpm"
|
||||
rm -f "$dir/${package}"-*."$machine.rpm"
|
||||
ln -sf "$package.rpm" "$dir/$package-$pkgver.$machine.rpm"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
cfg=/var/lib/tailscale/relay.conf
|
||||
dir=$(dirname "$0")
|
||||
"$dir/taillogin" --config="$cfg"
|
||||
@@ -1,8 +0,0 @@
|
||||
# Set the port to listen on for incoming VPN packets.
|
||||
# Remote nodes will automatically be informed about the new port number,
|
||||
# but you might want to configure this in order to set external firewall
|
||||
# settings.
|
||||
PORT="--port=41641"
|
||||
|
||||
# Extra flags you might want to pass to relaynode.
|
||||
FLAGS=""
|
||||
@@ -1,40 +0,0 @@
|
||||
Name: tailscale-relay
|
||||
Version: 0.00
|
||||
Release: 0
|
||||
Summary: Traffic relay node for Tailscale
|
||||
Group: Network
|
||||
License: Proprietary
|
||||
URL: https://tailscale.com/
|
||||
Vendor: Tailscale Inc.
|
||||
#Source: https://github.com/tailscale/tailscale
|
||||
Source0: tailscale-relay.tar.gz
|
||||
#Prefix: %{_prefix}
|
||||
Packager: Avery Pennarun <apenwarr@tailscale.com>
|
||||
BuildRoot: %{_tmppath}/%{name}-root
|
||||
|
||||
%description
|
||||
Traffic relay node for Tailscale.
|
||||
|
||||
%prep
|
||||
%setup -n tailscale-relay
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
D=$RPM_BUILD_ROOT
|
||||
[ "$D" = "/" -o -z "$D" ] && exit 99
|
||||
rm -rf "$D"
|
||||
mkdir -p $D/usr/sbin $D/lib/systemd/system $D/etc/default $D/etc/tailscale
|
||||
cp taillogin tailscale-login relaynode $D/usr/sbin
|
||||
cp tailscale-relay.service $D/lib/systemd/system/
|
||||
cp tailscale-relay.defaults $D/etc/default/tailscale-relay
|
||||
|
||||
%clean
|
||||
|
||||
%files
|
||||
%defattr(-,root,root)
|
||||
%config(noreplace) /etc/default/tailscale-relay
|
||||
/lib/systemd/system/tailscale-relay.service
|
||||
/usr/sbin/taillogin
|
||||
/usr/sbin/tailscale-login
|
||||
/usr/sbin/relaynode
|
||||
@@ -1,7 +0,0 @@
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
redo-ifchange "$dir/$package.tar.gz"
|
||||
rm -f "$dir/$package"-*.tar.gz
|
||||
ln -sf "$package.tar.gz" "$dir/$package-$version.tar.gz"
|
||||
@@ -1,99 +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.
|
||||
|
||||
// The taillogin command, invoked via the tailscale-login shell script, is shipped
|
||||
// with the current (old) Linux client, to log in to Tailscale on a Linux box.
|
||||
//
|
||||
// Deprecated: this will be deleted, to be replaced by cmd/tailscale.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logpolicy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := getopt.StringLong("config", 'f', "", "path to config file")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailgate server")
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatal("too many non-flag arguments")
|
||||
}
|
||||
if *config == "" {
|
||||
log.Fatal("no --config file specified")
|
||||
}
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
defer pol.Close()
|
||||
|
||||
cfg, err := loadConfig(*config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hi := controlclient.NewHostinfo()
|
||||
hi.FrontendLogID = pol.PublicID.String()
|
||||
hi.BackendLogID = pol.PublicID.String()
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
c, err := controlclient.New(controlclient.Options{
|
||||
Persist: cfg,
|
||||
ServerURL: *server,
|
||||
Hostinfo: &hi,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c.SetStatusFunc(func(new controlclient.Status) {
|
||||
if new.URL != "" {
|
||||
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
|
||||
return
|
||||
}
|
||||
if new.Err != "" {
|
||||
log.Print(new.Err)
|
||||
return
|
||||
}
|
||||
if new.Persist != nil {
|
||||
if err := saveConfig(*config, *new.Persist); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
if new.NetMap != nil {
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
c.Login(nil, 0)
|
||||
<-done
|
||||
log.Printf("Success.\n")
|
||||
}
|
||||
|
||||
func loadConfig(path string) (cfg controlclient.Persist, err error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("config %s does not exist", path)
|
||||
return controlclient.Persist{}, nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(path string, cfg controlclient.Persist) error {
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
123
cmd/tailscale/cli/cli.go
Normal file
123
cmd/tailscale/cli/cli.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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", "status", "netcheck", "version",
|
||||
"-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,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
versionCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
162
cmd/tailscale/cli/netcheck.go
Normal file
162
cmd/tailscale/cli/netcheck.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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/dnscache"
|
||||
"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{
|
||||
DNSCache: dnscache.Get(),
|
||||
}
|
||||
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 (%v)\n", report.PreferredDERP, dm.Regions[report.PreferredDERP].RegionCode)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
}
|
||||
sort.Ints(rids)
|
||||
for _, rid := range rids {
|
||||
d, ok := report.RegionLatency[rid]
|
||||
var latency string
|
||||
if ok {
|
||||
latency = d.Round(time.Millisecond / 10).String()
|
||||
}
|
||||
fmt.Printf("\t\t- %v, %3s = %s\n", rid, dm.Regions[rid].RegionCode, latency)
|
||||
}
|
||||
}
|
||||
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, ", ")
|
||||
}
|
||||
172
cmd/tailscale/cli/status.go
Normal file
172
cmd/tailscale/cli/status.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// 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"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
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.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
|
||||
})(),
|
||||
}
|
||||
|
||||
var statusArgs struct {
|
||||
json bool // JSON output mode
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
ch <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
getStatus := func() (*ipnstate.Status, error) {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-ch:
|
||||
return st, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
st, err := getStatus()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
fmt.Printf("%s", j)
|
||||
return nil
|
||||
}
|
||||
if statusArgs.web {
|
||||
ln, err := net.Listen("tcp", statusArgs.listen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statusURL := interfaces.HTTPOfListener(ln)
|
||||
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}()
|
||||
if statusArgs.browser {
|
||||
go webbrowser.Open(statusURL)
|
||||
}
|
||||
err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.RequestURI != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
st, err := getStatus()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st.WriteHTML(w)
|
||||
}))
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
peer.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(", ")
|
||||
}
|
||||
if addr == ps.CurAddr {
|
||||
f("*%s*", addr)
|
||||
} else {
|
||||
f("%s", addr)
|
||||
}
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
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
|
||||
}
|
||||
252
cmd/tailscale/cli/up.go
Normal file
252
cmd/tailscale/cli/up.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// 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 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.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")
|
||||
upf.BoolVar(&upArgs.enableDERP, "enable-derp", true, "enable the use of DERP servers")
|
||||
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) {
|
||||
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", "on", "netfilter mode (one of on, nodivert, off)")
|
||||
}
|
||||
return upf
|
||||
})(),
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
enableDERP bool
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
// parseIPOrCIDR parses an IP address or a CIDR prefix. If the input
|
||||
// is an IP address, it is returned in CIDR form with a /32 mask for
|
||||
// IPv4 or a /128 mask for IPv6.
|
||||
func parseIPOrCIDR(s string) (wgcfg.CIDR, bool) {
|
||||
if strings.Contains(s, "/") {
|
||||
ret, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return wgcfg.CIDR{}, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
ip, ok := wgcfg.ParseIP(s)
|
||||
if !ok {
|
||||
return wgcfg.CIDR{}, false
|
||||
}
|
||||
if ip.Is4() {
|
||||
return wgcfg.CIDR{IP: ip, Mask: 32}, true
|
||||
} else {
|
||||
return wgcfg.CIDR{IP: ip, Mask: 128}, true
|
||||
}
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
|
||||
func warning(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings on linux 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) {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
warning("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 {
|
||||
warning("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
if !on {
|
||||
warning("%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)
|
||||
}
|
||||
|
||||
var routes []wgcfg.CIDR
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
checkIPForwarding()
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
cidr, ok := parseIPOrCIDR(s)
|
||||
if !ok {
|
||||
log.Fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
log.Fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
// 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.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.DisableDERP = !upArgs.enableDERP
|
||||
prefs.Hostname = upArgs.hostname
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
warning("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
warning("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
log.Fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
var printed bool
|
||||
|
||||
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:
|
||||
printed = true
|
||||
bc.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)
|
||||
}
|
||||
},
|
||||
}
|
||||
// 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
|
||||
}
|
||||
69
cmd/tailscale/cli/version.go
Normal file
69
cmd/tailscale/cli/version.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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.LONG)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Client: %s\n", version.LONG)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -7,134 +7,22 @@
|
||||
package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/safesocket"
|
||||
"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"
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
defer log.Printf("Control connection done.\n")
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
|
||||
socket := getopt.StringLong("socket", 0, "/run/tailscale/tailscaled.sock", "path of tailscaled's unix socket")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
|
||||
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
|
||||
routeall := getopt.BoolLong("remote-routes", 'R', "accept routes advertised by remote nodes")
|
||||
nopf := getopt.BoolLong("no-packet-filter", 'F', "disable packet filter")
|
||||
advroutes := getopt.ListLong("routes", 'r', "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.1.0/24)")
|
||||
getopt.Parse()
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer pol.Close()
|
||||
|
||||
var adv []wgcfg.CIDR
|
||||
for _, s := range *advroutes {
|
||||
cidr, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
log.Fatalf("%q is not a valid CIDR prefix: %v", s, err)
|
||||
}
|
||||
adv = append(adv, *cidr)
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
// TODO(apenwarr): allow setting/using CorpDNS
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = *server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = *routeall
|
||||
prefs.AllowSingleHosts = !*nuroutes
|
||||
prefs.UsePacketFilter = !*nopf
|
||||
prefs.AdvertiseRoutes = adv
|
||||
|
||||
c, err := safesocket.Connect(*socket, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
bc.SetPrefs(prefs)
|
||||
opts := ipn.Options{
|
||||
StateKey: globalStateKey,
|
||||
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", *server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
fmt.Fprintf(os.Stderr, "\ntailscaled is authenticated, nothing more to do.\n\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)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# settings.
|
||||
PORT="41641"
|
||||
|
||||
# Extra flags you might want to pass to relaynode.
|
||||
# Extra flags you might want to pass to tailscaled.
|
||||
FLAGS=""
|
||||
|
||||
@@ -14,13 +14,22 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
@@ -32,78 +41,156 @@ import (
|
||||
// later, the global state key doesn't look like a username.
|
||||
const globalStateKey = "_daemon"
|
||||
|
||||
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, "tailscale0", "tunnel interface name")
|
||||
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
|
||||
statepath := getopt.StringLong("state", 0, "", "Path of state file")
|
||||
socketpath := getopt.StringLong("socket", 's', "tailscaled.sock", "Path of the service unix socket")
|
||||
// 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"
|
||||
}
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
var args struct {
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Set default values for getopt.
|
||||
args.tunname = defaultTunName()
|
||||
args.port = magicsock.DefaultPort
|
||||
args.statepath = paths.DefaultTailscaledStateFile()
|
||||
args.socketpath = paths.DefaultTailscaledSocket()
|
||||
|
||||
getopt.FlagLong(&args.cleanup, "cleanup", 0, "clean up system state and exit")
|
||||
getopt.FlagLong(&args.fake, "fake", 0, "fake tunnel+routing instead of tuntap")
|
||||
getopt.FlagLong(&args.debug, "debug", 0, "address of debug server")
|
||||
getopt.FlagLong(&args.tunname, "tun", 0, "tunnel interface name")
|
||||
getopt.FlagLong(&args.port, "port", 'p', "WireGuard port (0=autoselect)")
|
||||
getopt.FlagLong(&args.statepath, "state", 0, "path of state file")
|
||||
getopt.FlagLong(&args.socketpath, "socket", 's', "path of the service unix socket")
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
logf("fixConsoleOutput: %v\n", err)
|
||||
log.Fatalf("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])
|
||||
}
|
||||
|
||||
if *statepath == "" {
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
if *socketpath == "" {
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
if *debug != "" {
|
||||
go runDebugServer(*debug)
|
||||
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")
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
router.Cleanup(logf, args.tunname)
|
||||
return nil
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
var e wgengine.Engine
|
||||
if *fake {
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
} 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\n", 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.
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-interrupt:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// continue
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: *socketpath,
|
||||
StatePath: *statepath,
|
||||
SocketPath: args.socketpath,
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
LegacyConfigPath: "/var/lib/tailscale/relay.conf",
|
||||
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\n", err)
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), opts, e)
|
||||
// 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 runDebugServer(addr string) {
|
||||
func newDebugMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
srv := http.Server{
|
||||
return mux
|
||||
}
|
||||
|
||||
func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ StartLimitBurst=0
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
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
|
||||
|
||||
@@ -16,8 +17,8 @@ RuntimeDirectory=tailscale
|
||||
RuntimeDirectoryMode=0755
|
||||
StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
User=root
|
||||
Group=root
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"tailscale.com/interfaces"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -17,43 +17,46 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// 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))
|
||||
@@ -61,13 +64,14 @@ func (s state) String() string {
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message
|
||||
Err string
|
||||
URL string
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo tailcfg.Hostinfo // current Hostinfo data
|
||||
state state
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
State State
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
@@ -82,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 {
|
||||
@@ -90,10 +94,11 @@ 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 {
|
||||
_ structs.Incomparable
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *oauth2.Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
@@ -115,10 +120,10 @@ type Client 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
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state state
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -147,6 +152,9 @@ func NewNoStart(opts Options) (*Client, error) {
|
||||
if opts.Logf == nil {
|
||||
opts.Logf = func(fmt string, args ...interface{}) {}
|
||||
}
|
||||
if opts.TimeNow == nil {
|
||||
opts.TimeNow = time.Now
|
||||
}
|
||||
c := &Client{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
@@ -199,7 +207,7 @@ func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("cancelMapSafely: synced=%v\n", c.synced)
|
||||
c.logf("cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
// received at least one netmap since the last
|
||||
@@ -221,23 +229,23 @@ func (c *Client) cancelMapSafely() {
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("cancelMapSafely: wrote to channel\n")
|
||||
c.logf("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\n")
|
||||
c.logf("cancelMapSafely: channel was full")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.Backoff{Name: "authRoutine"}
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s\n", c.state)
|
||||
c.logf("authRoutine: %s", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
@@ -246,13 +254,13 @@ func (c *Client) authRoutine() {
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("authRoutine: quit\n")
|
||||
c.logf("authRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v\n", msg, err)
|
||||
c.logf("%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.
|
||||
@@ -281,14 +289,14 @@ func (c *Client) authRoutine() {
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("authRoutine: context done.\n")
|
||||
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.\n")
|
||||
c.logf("authRoutine: key expiration check.")
|
||||
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
|
||||
c.logf("Key expired; setting loggedIn=false.")
|
||||
|
||||
@@ -313,7 +321,7 @@ 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()
|
||||
|
||||
@@ -322,9 +330,9 @@ func (c *Client) authRoutine() {
|
||||
} 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()
|
||||
|
||||
@@ -347,13 +355,14 @@ 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()
|
||||
|
||||
@@ -366,7 +375,7 @@ 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)
|
||||
@@ -376,26 +385,40 @@ func (c *Client) authRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (c *Client) mapRoutine() {
|
||||
defer close(c.mapDone)
|
||||
bo := backoff.Backoff{Name: "mapRoutine"}
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("mapRoutine: %s\n", c.state)
|
||||
c.logf("mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
ctx := c.mapCtx
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("mapRoutine: quit\n")
|
||||
c.logf("mapRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v\n", msg, err)
|
||||
c.logf("%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.
|
||||
@@ -413,9 +436,9 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("mapRoutine: context done.\n")
|
||||
c.logf("mapRoutine: context done.")
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map needed while idle.\n")
|
||||
c.logf("mapRoutine: new map needed while idle.")
|
||||
}
|
||||
} else {
|
||||
// Be sure this is false when we're not inside
|
||||
@@ -430,7 +453,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.\n")
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.cancelMapLocked()
|
||||
|
||||
// Don't emit this netmap; we're
|
||||
@@ -443,7 +466,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
|
||||
@@ -452,7 +475,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("mapRoutine: netmap received: %s\n", state)
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine2", nil, "", nm)
|
||||
}
|
||||
@@ -461,8 +484,8 @@ func (c *Client) mapRoutine() {
|
||||
c.mu.Lock()
|
||||
c.synced = false
|
||||
c.inPollNetMap = false
|
||||
if c.state == stateSynchronized {
|
||||
c.state = stateAuthenticated
|
||||
if c.state == StateSynchronized {
|
||||
c.state = StateAuthenticated
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -489,12 +512,34 @@ func (c *Client) SetStatusFunc(fn func(Status)) {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) SetHostinfo(hi tailcfg.Hostinfo) {
|
||||
c.direct.SetHostinfo(hi)
|
||||
func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
if !c.direct.SetHostinfo(hi) {
|
||||
c.logf("[unexpected] duplicate Hostinfo: %v", hi)
|
||||
return
|
||||
}
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
// Send new Hostinfo to server
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
if ni == nil {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
if !c.direct.SetNetInfo(ni) {
|
||||
c.logf("[unexpected] duplicate NetInfo: %v", ni)
|
||||
return
|
||||
}
|
||||
c.logf("NetInfo: %v", ni)
|
||||
|
||||
// Send new Hostinfo (which includes NetInfo) to server
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.mu.Lock()
|
||||
state := c.state
|
||||
@@ -505,11 +550,11 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("sendStatus: %s: %v\n", who, state)
|
||||
c.logf("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 {
|
||||
@@ -526,7 +571,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()
|
||||
@@ -541,7 +586,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
}
|
||||
|
||||
func (c *Client) Login(t *oauth2.Token, flags LoginFlags) {
|
||||
c.logf("client.Login(%v, %v)\n", t != nil, flags)
|
||||
c.logf("client.Login(%v, %v)", t != nil, flags)
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
@@ -555,7 +600,7 @@ func (c *Client) Login(t *oauth2.Token, flags LoginFlags) {
|
||||
}
|
||||
|
||||
func (c *Client) Logout() {
|
||||
c.logf("client.Logout()\n")
|
||||
c.logf("client.Logout()")
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
@@ -574,7 +619,7 @@ func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
|
||||
}
|
||||
|
||||
func (c *Client) Shutdown() {
|
||||
c.logf("client.Shutdown()\n")
|
||||
c.logf("client.Shutdown()")
|
||||
|
||||
c.mu.Lock()
|
||||
inSendStatus := c.inSendStatus
|
||||
@@ -585,13 +630,30 @@ func (c *Client) Shutdown() {
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("client.Shutdown: inSendStatus=%v\n", inSendStatus)
|
||||
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
|
||||
if !closed {
|
||||
close(c.quit)
|
||||
c.cancelAuth()
|
||||
<-c.authDone
|
||||
c.cancelMapUnsafely()
|
||||
<-c.mapDone
|
||||
c.logf("Client.Shutdown done.\n")
|
||||
c.logf("Client.Shutdown done.")
|
||||
}
|
||||
}
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Client) TestOnlyNodePublicKey() wgcfg.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
@@ -13,14 +13,16 @@ import (
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fields = append(fields, t.Field(i).Name)
|
||||
if name := t.Field(i).Name; name != "_" {
|
||||
fields = append(fields, name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -46,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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -26,13 +27,17 @@ import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"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
|
||||
@@ -76,29 +81,35 @@ type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgcfg.Key
|
||||
persist Persist
|
||||
authKey string
|
||||
tryingNewKey wgcfg.PrivateKey
|
||||
expiry *time.Time
|
||||
hostinfo tailcfg.Hostinfo
|
||||
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
|
||||
localPort uint16 // or zero to mean auto
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
HTTPC *http.Client // HTTP client used to talk to tailcontrol
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
TimeNow func() time.Time // time.Now implementation used by Client
|
||||
Hostinfo *tailcfg.Hostinfo
|
||||
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
|
||||
DiscoPublicKey tailcfg.DiscoKey
|
||||
NewDecompressor func() (Decompressor, error)
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -112,61 +123,91 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
return nil, errors.New("controlclient.New: no server URL specified")
|
||||
}
|
||||
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
|
||||
if opts.HTTPC == nil {
|
||||
opts.HTTPC = http.DefaultClient
|
||||
serverURL, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.TimeNow == nil {
|
||||
opts.TimeNow = time.Now
|
||||
}
|
||||
if opts.Logf == nil {
|
||||
// TODO(apenwarr): remove this default and fail instead.
|
||||
// TODO(bradfitz): ... but then it shouldn't be in Options.
|
||||
opts.Logf = log.Printf
|
||||
}
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.DialContext = dialer.DialContext
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: opts.HTTPC,
|
||||
httpc: httpc,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
} else {
|
||||
c.SetHostinfo(*opts.Hostinfo)
|
||||
c.SetHostinfo(opts.Hostinfo)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewHostinfo() tailcfg.Hostinfo {
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
os := runtime.GOOS
|
||||
switch os {
|
||||
case "darwin":
|
||||
switch runtime.GOARCH {
|
||||
case "arm", "arm64":
|
||||
os = "iOS"
|
||||
default:
|
||||
os = "macOS"
|
||||
}
|
||||
}
|
||||
|
||||
return tailcfg.Hostinfo{
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.LONG,
|
||||
Hostname: hostname,
|
||||
OS: os,
|
||||
OS: version.OS(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Direct) SetHostinfo(hi tailcfg.Hostinfo) {
|
||||
// SetHostinfo clones the provided Hostinfo and remembers it for the
|
||||
// next update. It reports whether the Hostinfo has changed.
|
||||
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("Hostinfo: %v\n", hi)
|
||||
c.hostinfo = hi
|
||||
if hi.Equal(c.hostinfo) {
|
||||
return false
|
||||
}
|
||||
c.hostinfo = hi.Clone()
|
||||
return true
|
||||
}
|
||||
|
||||
// SetNetInfo clones the provided NetInfo and remembers it for the
|
||||
// next update. It reports whether the NetInfo has changed.
|
||||
func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
|
||||
if ni == nil {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.hostinfo == nil {
|
||||
c.logf("[unexpected] SetNetInfo called with no HostInfo; ignoring NetInfo update: %+v", ni)
|
||||
return false
|
||||
}
|
||||
if reflect.DeepEqual(ni, c.hostinfo.NetInfo) {
|
||||
return false
|
||||
}
|
||||
c.hostinfo.NetInfo = ni.Clone()
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Direct) GetPersist() Persist {
|
||||
@@ -183,7 +224,7 @@ const (
|
||||
)
|
||||
|
||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
c.logf("direct.TryLogout()\n")
|
||||
c.logf("direct.TryLogout()")
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -199,12 +240,12 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(%v, %v)\n", t != nil, flags)
|
||||
c.logf("direct.TryLogin(%v, %v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, t, flags, false, "")
|
||||
}
|
||||
|
||||
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newUrl string, err error) {
|
||||
c.logf("direct.WaitLoginURL\n")
|
||||
c.logf("direct.WaitLoginURL")
|
||||
return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url)
|
||||
}
|
||||
|
||||
@@ -224,11 +265,14 @@ 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.\n")
|
||||
c.logf("Generating a new machinekey.")
|
||||
mkey, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -237,15 +281,15 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
}
|
||||
|
||||
if expired {
|
||||
c.logf("Old key expired -> regen=true\n")
|
||||
c.logf("Old key expired -> regen=true")
|
||||
regen = true
|
||||
}
|
||||
if (flags & LoginInteractive) != 0 {
|
||||
c.logf("LoginInteractive -> regen=true\n")
|
||||
c.logf("LoginInteractive -> regen=true")
|
||||
regen = true
|
||||
}
|
||||
|
||||
c.logf("doLogin(regen=%v, hasUrl=%v)\n", regen, url != "")
|
||||
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
|
||||
if serverKey == (wgcfg.Key{}) {
|
||||
var err error
|
||||
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
|
||||
@@ -261,7 +305,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
var oldNodeKey wgcfg.Key
|
||||
if url != "" {
|
||||
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
|
||||
c.logf("Generating a new nodekey.\n")
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
@@ -278,9 +322,9 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
}
|
||||
|
||||
if tryingNewKey == (wgcfg.PrivateKey{}) {
|
||||
log.Fatalf("tryingNewKey is empty, give up\n")
|
||||
log.Fatalf("tryingNewKey is empty, give up")
|
||||
}
|
||||
if c.hostinfo.BackendLogID == "" {
|
||||
if backendLogID == "" {
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, url, err
|
||||
}
|
||||
@@ -288,15 +332,16 @@ 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\n",
|
||||
request.OldNodeKey.AbbrevString(),
|
||||
request.NodeKey.AbbrevString(), url != "")
|
||||
c.logf("RegisterReq: onode=%v node=%v fup=%v",
|
||||
request.OldNodeKey.ShortString(),
|
||||
request.NodeKey.ShortString(), url != "")
|
||||
request.Auth.Oauth2Token = t
|
||||
request.Auth.Provider = persist.Provider
|
||||
request.Auth.LoginName = persist.LoginName
|
||||
request.Auth.AuthKey = authKey
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
@@ -314,7 +359,7 @@ 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.\n")
|
||||
c.logf("RegisterReq: returned.")
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
|
||||
return regen, url, fmt.Errorf("register request: %v", err)
|
||||
@@ -325,7 +370,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
return true, "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
|
||||
}
|
||||
c.logf("server reports new node key %v has expired",
|
||||
request.NodeKey.AbbrevString())
|
||||
request.NodeKey.ShortString())
|
||||
return true, "", nil
|
||||
}
|
||||
if persist.Provider == "" {
|
||||
@@ -342,9 +387,9 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
// - user is disabled
|
||||
|
||||
if resp.AuthURL != "" {
|
||||
c.logf("AuthURL is %.20v...\n", resp.AuthURL)
|
||||
c.logf("AuthURL is %v", resp.AuthURL)
|
||||
} else {
|
||||
c.logf("No AuthURL\n")
|
||||
c.logf("No AuthURL")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -391,7 +436,7 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
|
||||
if c.localPort == localPort && sameStrings(c.endpoints, endpoints) {
|
||||
return false // unchanged
|
||||
}
|
||||
c.logf("client.newEndpoints(%v, %v)\n", localPort, endpoints)
|
||||
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
|
||||
c.localPort = localPort
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
return true // changed
|
||||
@@ -411,25 +456,34 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
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...)
|
||||
c.mu.Unlock()
|
||||
|
||||
if hostinfo.BackendLogID == "" {
|
||||
if backendLogID == "" {
|
||||
return errors.New("hostinfo: BackendLogID missing")
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("PollNetMap: stream=%v :%v %v\n", maxPolls, localPort, ep)
|
||||
c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if Debug.NetMap {
|
||||
vlogf = c.logf
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
Version: 4,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
Version: 4,
|
||||
IncludeIPv6: true,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
DebugForceDisco: Debug.ForceDisco,
|
||||
}
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
@@ -437,9 +491,11 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
@@ -451,8 +507,10 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
|
||||
res, err := c.httpc.Do(req)
|
||||
if err != nil {
|
||||
vlogf("netmap: Do: %v", err)
|
||||
return err
|
||||
}
|
||||
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
@@ -467,26 +525,35 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
const pollTimeout = 120 * time.Second
|
||||
timeout := time.NewTimer(pollTimeout)
|
||||
timeoutReset := make(chan struct{})
|
||||
defer close(timeoutReset)
|
||||
pollDone := make(chan struct{})
|
||||
defer close(pollDone)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-pollDone:
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
case <-timeout.C:
|
||||
c.logf("map response long-poll timed out!")
|
||||
cancel()
|
||||
return
|
||||
case _, ok := <-timeoutReset:
|
||||
if !ok {
|
||||
return // channel closed, shut down goroutine
|
||||
}
|
||||
case <-timeoutReset:
|
||||
if !timeout.Stop() {
|
||||
<-timeout.C
|
||||
select {
|
||||
case <-timeout.C:
|
||||
case <-pollDone:
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
}
|
||||
}
|
||||
vlogf("netmap: reset timeout timer")
|
||||
timeout.Reset(pollTimeout)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
|
||||
// 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.
|
||||
@@ -495,33 +562,58 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
// We can use this same read loop either way.
|
||||
var msg []byte
|
||||
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
|
||||
if _, err := io.ReadFull(res.Body, siz[:]); err != nil {
|
||||
vlogf("netmap: size read error after %v: %v", time.Since(t0).Round(time.Millisecond), err)
|
||||
return err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(siz[:])
|
||||
vlogf("netmap: read size %v after %v", size, time.Since(t0).Round(time.Millisecond))
|
||||
msg = append(msg[:0], make([]byte, size)...)
|
||||
if _, err := io.ReadFull(res.Body, msg); err != nil {
|
||||
vlogf("netmap: body read error: %v", err)
|
||||
return err
|
||||
}
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
|
||||
// Default filter if the key is missing from the incoming
|
||||
// json (ie. old tailcontrol server without PacketFilter
|
||||
// support). If even an empty PacketFilter is provided, this
|
||||
// will be overwritten.
|
||||
// TODO(apenwarr 2020-02-01): remove after tailcontrol is fully deployed.
|
||||
resp.PacketFilter = filter.MatchAllowAll
|
||||
|
||||
if err := c.decodeMsg(msg, &resp); err != nil {
|
||||
vlogf("netmap: decode error: %v")
|
||||
return err
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
c.logf("map response keep alive received")
|
||||
timeoutReset <- struct{}{}
|
||||
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()
|
||||
}
|
||||
continue
|
||||
}
|
||||
vlogf("netmap: got new map")
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
lastDERPMap = resp.DERPMap
|
||||
}
|
||||
if resp.Debug != nil && resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
nm := &NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
@@ -537,15 +629,9 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
DNS: resp.DNS,
|
||||
DNSDomains: resp.SearchPaths,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: resp.PacketFilter,
|
||||
}
|
||||
// Temporary (2020-02-21) knob to force debug, during DERP testing:
|
||||
if ok, _ := strconv.ParseBool(os.Getenv("DEBUG_FORCE_DERP")); ok {
|
||||
c.logf("debug: adding DERP endpoints to all peers")
|
||||
for i := range nm.Peers {
|
||||
peer := &nm.Peers[i]
|
||||
peer.Endpoints = append([]string{"127.3.3.40:1"}, peer.Endpoints...)
|
||||
}
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
for _, profile := range resp.UserProfiles {
|
||||
nm.UserProfiles[profile.ID] = profile
|
||||
@@ -555,7 +641,17 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
//c.logf("new network map[%d]:\n%s", i, nm.Concise())
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
// Code elsewhere prints netmap diffs every time, so this
|
||||
// occasional full dump, plus incremental diffs, should do
|
||||
// the job.
|
||||
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.mu.Lock()
|
||||
c.expiry = &nm.Expiry
|
||||
@@ -582,8 +678,10 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
|
||||
}
|
||||
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
c.mu.Lock()
|
||||
mkey := c.persist.PrivateMachineKey
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
decrypted, err := decryptMsg(msg, &serverKey, &mkey)
|
||||
if err != nil {
|
||||
@@ -593,7 +691,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
|
||||
@@ -643,6 +740,12 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const debugMapRequests = false
|
||||
if debugMapRequests {
|
||||
if _, ok := v.(tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
panic(err)
|
||||
@@ -676,3 +779,39 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
ForceDisco bool // ask control server to not filter out our disco key
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
d := debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
|
||||
ForceDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
if d.ForceDisco || os.Getenv("TS_DEBUG_USE_DISCO") == "" {
|
||||
// This is now defaults to on.
|
||||
d.Disco = true
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,305 +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 controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.io/control" // not yet released
|
||||
)
|
||||
|
||||
func TestClientsReusingKeys(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)
|
||||
}))
|
||||
server, err = control.New(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"
|
||||
c1, err := NewDirect(Options{
|
||||
ServerURL: httpsrv.URL,
|
||||
HTTPC: 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)
|
||||
}
|
||||
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, httpsrv.Client(), user, authURL)
|
||||
newURL, err := c1.WaitLoginURL(ctx, authURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newURL != "" {
|
||||
t.Fatalf("unexpected newURL: %s", newURL)
|
||||
}
|
||||
|
||||
pollErrCh := make(chan error)
|
||||
go func() {
|
||||
err := c1.PollNetMap(ctx, -1, func(netMap *NetworkMap) {})
|
||||
pollErrCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-pollErrCh:
|
||||
t.Fatal(err)
|
||||
default:
|
||||
}
|
||||
|
||||
c2, err := NewDirect(Options{
|
||||
ServerURL: httpsrv.URL,
|
||||
HTTPC: 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)
|
||||
}
|
||||
if authURL != "" {
|
||||
t.Errorf("unexpected authURL %s", authURL)
|
||||
}
|
||||
|
||||
err = c2.PollNetMap(ctx, 1, func(netMap *NetworkMap) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-pollErrCh:
|
||||
t.Logf("expected poll error: %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)
|
||||
}))
|
||||
server, err = control.New(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,
|
||||
HTTPC: httpsrv.Client(),
|
||||
//TimeNow: s.control.TimeNow,
|
||||
Logf: func(fmt string, args ...interface{}) {
|
||||
t.Helper()
|
||||
t.Logf("c1: "+fmt, args...)
|
||||
},
|
||||
Hostinfo: &hi,
|
||||
}
|
||||
}
|
||||
|
||||
// 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, httpsrv.Client(), 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, httpsrv.Client(), 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, httpsrv.Client(), 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, httpsrv.Client(), 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))
|
||||
}
|
||||
}
|
||||
84
control/controlclient/filter.go
Normal file
84
control/controlclient/filter.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 (
|
||||
"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)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
@@ -5,18 +5,18 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -29,12 +29,19 @@ type NetworkMap struct {
|
||||
Addresses []wgcfg.CIDR
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
Peers []tailcfg.Node
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS []wgcfg.IP
|
||||
DNSDomains []string
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
PacketFilter filter.Matches
|
||||
|
||||
// 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
|
||||
|
||||
User tailcfg.UserID
|
||||
@@ -47,55 +54,148 @@ type NetworkMap struct {
|
||||
// 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 keyString(key [32]byte) string {
|
||||
b64 := base64.StdEncoding.EncodeToString(key[:])
|
||||
abbrev := "invalid"
|
||||
if len(b64) == 44 {
|
||||
abbrev = b64[0:4] + "…" + b64[39:43]
|
||||
}
|
||||
return fmt.Sprintf("[%s]", abbrev)
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) Concise() string {
|
||||
buf := new(strings.Builder)
|
||||
fmt.Fprintf(buf, "NetworkMap: self: %v auth=%v :%v %v\n",
|
||||
keyString(nm.NodeKey), 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 {
|
||||
aip[i] = fmt.Sprint(a)
|
||||
}
|
||||
u := fmt.Sprint(p.User)
|
||||
if strings.HasPrefix(u, "userid:") {
|
||||
u = "u:" + u[7:]
|
||||
}
|
||||
f1 := fmt.Sprintf(" %v %-6v %v",
|
||||
keyString(p.Key), u, p.Endpoints)
|
||||
f2 := fmt.Sprintf(" %*v\n", 70-len(f1),
|
||||
strings.Join(aip, " "))
|
||||
fmt.Fprintf(buf, "%s%s", f1, f2)
|
||||
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)
|
||||
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 ||
|
||||
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):]
|
||||
}
|
||||
|
||||
// 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, " "))
|
||||
}
|
||||
|
||||
// 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 &&
|
||||
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) &&
|
||||
eqStringsIgnoreNil(a.Endpoints, b.Endpoints)
|
||||
}
|
||||
|
||||
func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
|
||||
var diff strings.Builder
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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 _, pa := range aps {
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
}
|
||||
for _, pb := range bps {
|
||||
diff.WriteByte('+')
|
||||
printPeerConcise(&diff, pb)
|
||||
}
|
||||
return diff.String()
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) JSON() string {
|
||||
b, err := json.MarshalIndent(*nm, "", " ")
|
||||
if err != nil {
|
||||
@@ -104,184 +204,141 @@ func (nm *NetworkMap) JSON() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// TODO(apenwarr): delete me once relaynode doesn't need this anymore.
|
||||
// control.go:userMap() supercedes it. This does not belong in the client.
|
||||
func (nm *NetworkMap) UserMap() map[string][]filter.IP {
|
||||
// Make a lookup table of roles
|
||||
log.Printf("roles list is: %v\n", nm.Roles)
|
||||
roles := make(map[tailcfg.RoleID]tailcfg.Role)
|
||||
for _, role := range nm.Roles {
|
||||
roles[role.ID] = role
|
||||
}
|
||||
|
||||
// First, go through each node's addresses and make a lookup table
|
||||
// of IP->User.
|
||||
fwd := make(map[wgcfg.IP]string)
|
||||
for _, node := range nm.Peers {
|
||||
for _, addr := range node.Addresses {
|
||||
if addr.Mask == 32 && addr.IP.Is4() {
|
||||
user, ok := nm.UserProfiles[node.User]
|
||||
if ok {
|
||||
fwd[addr.IP] = user.LoginName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, reverse the mapping into User->IP.
|
||||
rev := make(map[string][]filter.IP)
|
||||
for ip, username := range fwd {
|
||||
ip4 := ip.To4()
|
||||
if ip4 != nil {
|
||||
fip := filter.NewIP(net.IP(ip4))
|
||||
rev[username] = append(rev[username], fip)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add roles, which are lists of users, and therefore lists
|
||||
// of those users' IP addresses.
|
||||
for _, user := range nm.UserProfiles {
|
||||
for _, roleid := range user.Roles {
|
||||
role, ok := roles[roleid]
|
||||
if ok {
|
||||
rolename := "role:" + role.Name
|
||||
rev[rolename] = append(rev[rolename], rev[user.LoginName]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("Usermap is: %v\n", rev)
|
||||
return rev
|
||||
}
|
||||
|
||||
var iOS = runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64")
|
||||
var keepalive = !iOS
|
||||
// WGConfigFlags is a bitmask of flags to control the behavior of the
|
||||
// wireguard configuration generation done by NetMap.WGCfg.
|
||||
type WGConfigFlags int
|
||||
|
||||
const (
|
||||
UAllowSingleHosts = 1 << iota
|
||||
UAllowSubnetRoutes
|
||||
UAllowDefaultRoute
|
||||
UHackDefaultRoute
|
||||
|
||||
UDefault = 0
|
||||
AllowSingleHosts WGConfigFlags = 1 << iota
|
||||
AllowSubnetRoutes
|
||||
AllowDefaultRoute
|
||||
HackDefaultRoute
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) UAPI(uflags int, dnsOverride []wgcfg.IP) string {
|
||||
wgcfg, err := nm.WGCfg(uflags, dnsOverride)
|
||||
// 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(flags WGConfigFlags, dnsOverride []wgcfg.IP) string {
|
||||
wgcfg, err := nm.WGCfg(log.Printf, flags, dnsOverride)
|
||||
if err != nil {
|
||||
log.Fatalf("WGCfg() failed unexpectedly: %v\n", err)
|
||||
log.Fatalf("WGCfg() failed unexpectedly: %v", err)
|
||||
}
|
||||
s, err := wgcfg.ToUAPI()
|
||||
if err != nil {
|
||||
log.Fatalf("ToUAPI() failed unexpectedly: %v\n", err)
|
||||
log.Fatalf("ToUAPI() failed unexpectedly: %v", 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")
|
||||
}
|
||||
// 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(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")
|
||||
// WGCfg returns the NetworkMaps's Wireguard configuration.
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: nm.PrivateKey,
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
DNS: append([]wgcfg.IP(nil), dnsOverride...),
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
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.AbbrevString())
|
||||
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[:]))
|
||||
if len(peer.Endpoints) > 0 {
|
||||
if len(peer.Endpoints) == 1 {
|
||||
fmt.Fprintf(buf, "Endpoint = %s", peer.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(peer.Endpoints, ","))
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Endpoint = %s # other endpoints: %s",
|
||||
peer.Endpoints[0],
|
||||
strings.Join(peer.Endpoints[1:], ", "))
|
||||
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
|
||||
PublicKey: wgcfg.Key(peer.Key),
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
if peer.KeepAlive {
|
||||
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
|
||||
}
|
||||
}
|
||||
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.AbbrevString())
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("wgcfg: %v skipping default route", 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.AbbrevString(), aip)
|
||||
if (flags & HackDefaultRoute) != 0 {
|
||||
allowedIP = wgcfg.CIDR{IP: wgcfg.IPv4(10, 0, 0, 0), Mask: 8}
|
||||
logf("wgcfg: %v converting default route => %v", peer.Key.ShortString(), allowedIP.String())
|
||||
}
|
||||
} else if allowedIP.Mask < 32 {
|
||||
if (uflags & UAllowSubnetRoutes) == 0 {
|
||||
log.Printf("wgcfg: %v skipping subnet route\n", peer.Key.AbbrevString())
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
aips = append(aips, aip)
|
||||
}
|
||||
fmt.Fprintf(buf, "AllowedIPs = %s\n", strings.Join(aips, ", "))
|
||||
if keepalive {
|
||||
fmt.Fprintf(buf, "PersistentKeepalive = 25\n")
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
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 []wgcfg.CIDR) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
217
control/controlclient/netmap_test.go
Normal file
217
control/controlclient/netmap_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func testNodeKey(b byte) (ret tailcfg.NodeKey) {
|
||||
for i := range ret {
|
||||
ret[i] = 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 []\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 debug={} []\n",
|
||||
},
|
||||
{
|
||||
name: "debug_values",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{LogHeapPprof: true},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown 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 []\n+netmap: self: [AgICA] auth=machine-unknown []\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",
|
||||
},
|
||||
} {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
74
derp/derp.go
74
derp/derp.go
@@ -32,10 +32,20 @@ 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.
|
||||
// * 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
|
||||
)
|
||||
|
||||
// frameType is the one byte frame type at the beginning of the frame
|
||||
@@ -58,12 +68,39 @@ Steady state:
|
||||
* server then sends frameRecvPacket to recipient
|
||||
*/
|
||||
const (
|
||||
frameServerKey = frameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
|
||||
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
|
||||
frameRecvPacket = frameType(0x05) // packet bytes
|
||||
frameKeepAlive = frameType(0x06) // no payload, no-op (to be replaced with ping/pong)
|
||||
frameServerKey = frameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
|
||||
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
|
||||
|
||||
// framePeerGone is sent from server to client to signal that
|
||||
// a previous sender is no longer connected. That is, if A
|
||||
// sent to B, and then if A disconnects, the server sends
|
||||
// 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
|
||||
@@ -121,7 +158,8 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
|
||||
if frameLen > maxSize {
|
||||
return 0, 0, fmt.Errorf("frame header size %d exceeds reader limit of %d", frameLen, maxSize)
|
||||
}
|
||||
n, err := io.ReadFull(br, b[:frameLen])
|
||||
|
||||
n, err := io.ReadFull(br, b[:minUint32(frameLen, uint32(len(b)))])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -155,3 +193,17 @@ 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
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
@@ -19,18 +19,57 @@ 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
|
||||
logf logger.Logf
|
||||
nc net.Conn
|
||||
br *bufio.Reader
|
||||
bw *bufio.Writer
|
||||
readErr error // sticky read error
|
||||
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
|
||||
meshKey string
|
||||
|
||||
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 net.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
|
||||
}
|
||||
|
||||
// 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 }) }
|
||||
|
||||
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(),
|
||||
@@ -38,19 +77,19 @@ func NewClient(privateKey key.Private, nc net.Conn, brw *bufio.ReadWriter, logf
|
||||
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 err := c.sendClientKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
|
||||
}
|
||||
_, err := c.recvServerInfo()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -104,12 +143,28 @@ func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
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 {
|
||||
var nonce [nonceLen]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg := []byte("{}") // no clientInfo for now
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: protocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgbox := box.Seal(nil, msg, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
|
||||
buf := make([]byte, 0, nonceLen+keyLen+len(msgbox))
|
||||
@@ -119,6 +174,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.
|
||||
@@ -127,7 +185,7 @@ func (c *Client) Send(dstKey key.Public, pkt []byte) error { return c.send(dstKe
|
||||
func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
ret = fmt.Errorf("derp.Send: %v", ret)
|
||||
ret = fmt.Errorf("derp.Send: %w", ret)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -135,6 +193,9 @@ func (c *Client) send(dstKey key.Public, pkt []byte) (ret error) {
|
||||
return fmt.Errorf("packet too big: %d", len(pkt))
|
||||
}
|
||||
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(len(dstKey)+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,6 +208,85 @@ 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.
|
||||
func (c *Client) NotePreferred(preferred bool) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.NotePreferred: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameNotePreferred, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
var b byte = 0x00
|
||||
if preferred {
|
||||
b = 0x01
|
||||
}
|
||||
if err := c.bw.WriteByte(b); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
@@ -156,31 +296,89 @@ type ReceivedMessage interface {
|
||||
}
|
||||
|
||||
// ReceivedPacket is a ReceivedMessage representing an incoming packet.
|
||||
type ReceivedPacket []byte
|
||||
type ReceivedPacket struct {
|
||||
Source key.Public
|
||||
// Data is the received packet bytes. It aliases the memory
|
||||
// passed to Client.Recv.
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (ReceivedPacket) msg() {}
|
||||
|
||||
// PeerGoneMessage is a ReceivedMessage that indicates that the client
|
||||
// identified by the underlying public key had previously sent you a
|
||||
// packet but has now disconnected from the server.
|
||||
type PeerGoneMessage key.Public
|
||||
|
||||
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() {}
|
||||
|
||||
// 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
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.Recv: %v", err)
|
||||
err = fmt.Errorf("derp.Recv: %w", err)
|
||||
c.readErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
@@ -188,8 +386,37 @@ func (c *Client) Recv(b []byte) (m ReceivedMessage, err error) {
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
continue
|
||||
case framePeerGone:
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var pg PeerGoneMessage
|
||||
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:
|
||||
return ReceivedPacket(b[:n]), nil
|
||||
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]
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1148
derp/derp_server.go
1148
derp/derp_server.go
File diff suppressed because it is too large
Load Diff
@@ -6,15 +6,25 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"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()
|
||||
if _, err := crand.Read(k[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -35,13 +45,14 @@ func TestSendRecv(t *testing.T) {
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
var clients []*Client
|
||||
var connsOut []Conn
|
||||
var recvChs []chan []byte
|
||||
errCh := make(chan error, 3)
|
||||
|
||||
@@ -52,13 +63,15 @@ func TestSendRecv(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cout.Close()
|
||||
connsOut = append(connsOut, cout)
|
||||
|
||||
cin, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cin.Close()
|
||||
go s.Accept(cin, bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin)))
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
|
||||
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
|
||||
key := clientPrivateKeys[i]
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
|
||||
@@ -71,12 +84,13 @@ func TestSendRecv(t *testing.T) {
|
||||
t.Logf("Connected client %d.", i)
|
||||
}
|
||||
|
||||
var peerGoneCount expvar.Int
|
||||
|
||||
t.Logf("Starting read loops")
|
||||
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
|
||||
@@ -85,8 +99,13 @@ func TestSendRecv(t *testing.T) {
|
||||
default:
|
||||
t.Errorf("unexpected message type %T", m)
|
||||
continue
|
||||
case PeerGoneMessage:
|
||||
peerGoneCount.Add(1)
|
||||
case ReceivedPacket:
|
||||
recvChs[i] <- []byte(m)
|
||||
if m.Source.IsZero() {
|
||||
t.Errorf("zero Source address in ReceivedPacket")
|
||||
}
|
||||
recvChs[i] <- append([]byte(nil), m.Data...)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
@@ -112,6 +131,32 @@ func TestSendRecv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
wantActive := func(total, home int64) {
|
||||
t.Helper()
|
||||
dl := time.Now().Add(5 * time.Second)
|
||||
var gotTotal, gotHome int64
|
||||
for time.Now().Before(dl) {
|
||||
gotTotal, gotHome = s.curClients.Value(), s.curHomeClients.Value()
|
||||
if gotTotal == total && gotHome == home {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Errorf("total/home=%v/%v; want %v/%v", gotTotal, gotHome, total, home)
|
||||
}
|
||||
|
||||
wantClosedPeers := func(want int64) {
|
||||
t.Helper()
|
||||
var got int64
|
||||
dl := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(dl) {
|
||||
if got = peerGoneCount.Value(); got == want {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("peer gone count = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
msg1 := []byte("hello 0->1\n")
|
||||
if err := clients[0].Send(clientKeys[1], msg1); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -128,6 +173,572 @@ func TestSendRecv(t *testing.T) {
|
||||
recvNothing(0)
|
||||
recvNothing(1)
|
||||
|
||||
wantActive(3, 0)
|
||||
clients[0].NotePreferred(true)
|
||||
wantActive(3, 1)
|
||||
clients[0].NotePreferred(true)
|
||||
wantActive(3, 1)
|
||||
clients[0].NotePreferred(false)
|
||||
wantActive(3, 0)
|
||||
clients[0].NotePreferred(false)
|
||||
wantActive(3, 0)
|
||||
clients[1].NotePreferred(true)
|
||||
wantActive(3, 1)
|
||||
connsOut[1].Close()
|
||||
wantActive(2, 0)
|
||||
wantClosedPeers(1)
|
||||
clients[2].NotePreferred(true)
|
||||
wantActive(2, 1)
|
||||
clients[2].NotePreferred(false)
|
||||
wantActive(2, 0)
|
||||
connsOut[2].Close()
|
||||
wantActive(1, 0)
|
||||
wantClosedPeers(1)
|
||||
|
||||
t.Logf("passed")
|
||||
s.Close()
|
||||
|
||||
}
|
||||
|
||||
func TestSendFreeze(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
s.WriteTimeout = 100 * time.Millisecond
|
||||
|
||||
// We send two streams of messages:
|
||||
//
|
||||
// alice --> bob
|
||||
// alice --> cathy
|
||||
//
|
||||
// Then cathy stops processing messsages.
|
||||
// That should not interfere with alice talking to bob.
|
||||
|
||||
newClient := func(name string, k key.Private) (c *Client, clientConn nettest.Conn) {
|
||||
t.Helper()
|
||||
c1, c2 := nettest.NewConn(name, 1024)
|
||||
go s.Accept(c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(c2), bufio.NewWriter(c2))
|
||||
c, err := NewClient(k, c2, brw, t.Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c, c2
|
||||
}
|
||||
|
||||
aliceKey := newPrivateKey(t)
|
||||
aliceClient, aliceConn := newClient("alice", aliceKey)
|
||||
|
||||
bobKey := newPrivateKey(t)
|
||||
bobClient, bobConn := newClient("bob", bobKey)
|
||||
|
||||
cathyKey := newPrivateKey(t)
|
||||
cathyClient, cathyConn := newClient("cathy", cathyKey)
|
||||
|
||||
var (
|
||||
aliceCh = make(chan struct{}, 32)
|
||||
bobCh = make(chan struct{}, 32)
|
||||
cathyCh = make(chan struct{}, 32)
|
||||
)
|
||||
chs := func(name string) chan struct{} {
|
||||
switch name {
|
||||
case "alice":
|
||||
return aliceCh
|
||||
case "bob":
|
||||
return bobCh
|
||||
case "cathy":
|
||||
return cathyCh
|
||||
default:
|
||||
panic("unknown ch: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
errCh := make(chan error, 4)
|
||||
recv := func(name string, client *Client) {
|
||||
ch := chs(name)
|
||||
for {
|
||||
m, err := client.Recv()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("%s: %w", name, err)
|
||||
return
|
||||
}
|
||||
switch m := m.(type) {
|
||||
default:
|
||||
errCh <- fmt.Errorf("%s: unexpected message type %T", name, m)
|
||||
return
|
||||
case ReceivedPacket:
|
||||
if m.Source.IsZero() {
|
||||
errCh <- fmt.Errorf("%s: zero Source address in ReceivedPacket", name)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
go recv("alice", aliceClient)
|
||||
go recv("bob", bobClient)
|
||||
go recv("cathy", cathyClient)
|
||||
|
||||
var cancel func()
|
||||
go func() {
|
||||
t := time.NewTicker(2 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
var ctx context.Context
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-ctx.Done():
|
||||
errCh <- nil
|
||||
return
|
||||
}
|
||||
|
||||
msg1 := []byte("hello alice->bob\n")
|
||||
if err := aliceClient.Send(bobKey.Public(), msg1); err != nil {
|
||||
errCh <- fmt.Errorf("alice send to bob: %w", err)
|
||||
return
|
||||
}
|
||||
msg2 := []byte("hello alice->cathy\n")
|
||||
|
||||
// TODO: an error is expected here.
|
||||
// We ignore it, maybe we should log it somehow?
|
||||
aliceClient.Send(cathyKey.Public(), msg2)
|
||||
}
|
||||
}()
|
||||
|
||||
drainAny := func(ch chan struct{}) {
|
||||
// We are draining potentially infinite sources,
|
||||
// so place some reasonable upper limit.
|
||||
//
|
||||
// The important thing here is to make sure that
|
||||
// if any tokens remain in the channel, they
|
||||
// must have been generated after drainAny was
|
||||
// called.
|
||||
for i := 0; i < cap(ch); i++ {
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
drain := func(t *testing.T, name string) bool {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
// Ensure ch has at least one element.
|
||||
ch := chs(name)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-timer.C:
|
||||
t.Errorf("no packet received by %s", name)
|
||||
return false
|
||||
}
|
||||
// Drain remaining.
|
||||
drainAny(ch)
|
||||
return true
|
||||
}
|
||||
isEmpty := func(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-chs(name):
|
||||
t.Errorf("packet received by %s, want none", name)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("initial send", func(t *testing.T) {
|
||||
drain(t, "bob")
|
||||
drain(t, "cathy")
|
||||
isEmpty(t, "alice")
|
||||
})
|
||||
|
||||
t.Run("block cathy", func(t *testing.T) {
|
||||
// Block cathy. Now the cathyConn buffer will fill up quickly,
|
||||
// and the derp server will back up.
|
||||
cathyConn.SetReadBlock(true)
|
||||
time.Sleep(2 * s.WriteTimeout)
|
||||
|
||||
drain(t, "bob")
|
||||
drainAny(chs("cathy"))
|
||||
isEmpty(t, "alice")
|
||||
|
||||
// Now wait a little longer, and ensure packets still flow to bob
|
||||
if !drain(t, "bob") {
|
||||
t.Errorf("connection alice->bob frozen by alice->cathy")
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup, make sure we process all errors.
|
||||
t.Logf("TEST COMPLETE, cancelling sender")
|
||||
cancel()
|
||||
t.Logf("closing connections")
|
||||
aliceConn.Close()
|
||||
bobConn.Close()
|
||||
cathyConn.Close()
|
||||
|
||||
for i := 0; i < cap(errCh); i++ {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
continue
|
||||
}
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
return NewClient(priv, nc, brw, logf)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,18 +12,24 @@ package derphttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -35,32 +41,67 @@ import (
|
||||
// Send/Recv will completely re-establish the connection (unless Close
|
||||
// has been called).
|
||||
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
|
||||
closed chan struct{}
|
||||
url *url.URL
|
||||
resp *http.Response
|
||||
|
||||
netConnMu sync.Mutex
|
||||
netConn net.Conn
|
||||
// Either url or getRegion is non-nil:
|
||||
url *url.URL
|
||||
getRegion func() *tailcfg.DERPRegion
|
||||
|
||||
clientMu sync.Mutex
|
||||
client *derp.Client
|
||||
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
|
||||
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 {
|
||||
return nil, fmt.Errorf("derphttp.NewClient: %v", err)
|
||||
}
|
||||
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,
|
||||
logf: logf,
|
||||
url: u,
|
||||
closed: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -68,149 +109,508 @@ 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
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, err error) {
|
||||
// TODO: use ctx for TCP+TLS+HTTP below
|
||||
select {
|
||||
case <-c.closed:
|
||||
return nil, ErrClientClosed
|
||||
default:
|
||||
// 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
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
return "443"
|
||||
case "http":
|
||||
return "80"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
c.clientMu.Lock()
|
||||
defer c.clientMu.Unlock()
|
||||
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, 0, ErrClientClosed
|
||||
}
|
||||
if c.client != nil {
|
||||
return c.client, nil
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
c.logf("%s: connecting", caller)
|
||||
// timeout is the fallback maximum time (if ctx doesn't limit
|
||||
// it further) to do all of: DNS + TCP + TLS + HTTP Upgrade +
|
||||
// DERP upgrade.
|
||||
const timeout = 10 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Either timeout fired (handled below), or
|
||||
// we're returning via the defer cancel()
|
||||
// below.
|
||||
case <-c.ctx.Done():
|
||||
// Propagate a Client.Close call into
|
||||
// cancelling this context.
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
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
|
||||
|
||||
var netConn net.Conn
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s connect: %v", caller, err)
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
if ctx.Err() != nil {
|
||||
err = fmt.Errorf("%v: %v", ctx.Err(), err)
|
||||
}
|
||||
err = fmt.Errorf("%s connect to %v: %v", caller, c.targetString(reg), err)
|
||||
if tcpConn != nil {
|
||||
go tcpConn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.url.Scheme == "https" {
|
||||
port := c.url.Port()
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
config := &tls.Config{}
|
||||
var tlsConn *tls.Conn
|
||||
tlsConn, err = tls.Dial("tcp", net.JoinHostPort(c.url.Host, port), config)
|
||||
if tlsConn != nil {
|
||||
netConn = tlsConn
|
||||
}
|
||||
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 {
|
||||
netConn, err = net.Dial("tcp", c.url.Host)
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
c.netConnMu.Lock()
|
||||
c.netConn = netConn
|
||||
c.netConnMu.Unlock()
|
||||
// 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() {
|
||||
select {
|
||||
case <-done:
|
||||
// Normal path. Upgrade occurred in time.
|
||||
case <-ctx.Done():
|
||||
select {
|
||||
case <-done:
|
||||
// Normal path. Upgrade occurred in time.
|
||||
// But the ctx.Done() is also done because
|
||||
// the "defer cancel()" above scheduled
|
||||
// before this goroutine.
|
||||
default:
|
||||
// The TLS or HTTP or DERP exchanges didn't complete
|
||||
// in time. Force close the TCP connection to force
|
||||
// them to fail quickly.
|
||||
tcpConn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
conn := bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn))
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
if c.useHTTPS() {
|
||||
httpConn = c.tlsClient(tcpConn, node)
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", c.url.String(), nil)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(httpConn), bufio.NewWriter(httpConn))
|
||||
|
||||
req, err := http.NewRequest("GET", c.urlString(node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Upgrade", "WebSocket")
|
||||
req.Header.Set("Upgrade", "DERP")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
if err := req.Write(conn); err != nil {
|
||||
return nil, err
|
||||
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := conn.Flush(); err != nil {
|
||||
return nil, err
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.Reader, req)
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
|
||||
derpClient, err := derp.NewClient(c.privateKey, netConn, conn, c.logf)
|
||||
derpClient, err := derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
c.resp = resp
|
||||
if c.preferred {
|
||||
if err := derpClient.NotePreferred(true); err != nil {
|
||||
go httpConn.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
return c.client, nil
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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 (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
|
||||
}
|
||||
if err := client.Send(dstKey, b); err != nil {
|
||||
c.close()
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Recv(b []byte) (derp.ReceivedMessage, error) {
|
||||
client, err := c.connect(context.TODO(), "derphttp.Client.Recv")
|
||||
func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
m, err := client.Recv(b)
|
||||
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) {
|
||||
c.mu.Lock()
|
||||
if c.preferred == v {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.preferred = v
|
||||
client := c.client
|
||||
c.mu.Unlock()
|
||||
|
||||
if client != nil {
|
||||
if err := client.NotePreferred(v); err != nil {
|
||||
c.closeForReconnect(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.close()
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
select {
|
||||
case <-c.closed:
|
||||
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed {
|
||||
return ErrClientClosed
|
||||
default:
|
||||
}
|
||||
close(c.closed)
|
||||
c.close()
|
||||
c.closed = true
|
||||
if c.netConn != nil {
|
||||
c.netConn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) close() {
|
||||
c.netConnMu.Lock()
|
||||
netConn := c.netConn
|
||||
c.netConnMu.Unlock()
|
||||
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
}
|
||||
|
||||
c.clientMu.Lock()
|
||||
defer c.clientMu.Unlock()
|
||||
if c.client == nil {
|
||||
// closeForReconnect closes the underlying network connection and
|
||||
// zeros out the client field so future calls to Connect will
|
||||
// reconnect.
|
||||
//
|
||||
// The provided brokenClient is the client to forget. If current
|
||||
// client is not brokenClient, closeForReconnect does nothing. (This
|
||||
// prevents a send and receive goroutine from failing at the ~same
|
||||
// time and both calling closeForReconnect and the caller goroutines
|
||||
// forever calling closeForReconnect in lockstep endlessly;
|
||||
// https://github.com/tailscale/tailscale/pull/264)
|
||||
func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.client != brokenClient {
|
||||
return
|
||||
}
|
||||
c.resp = nil
|
||||
if c.netConn != nil {
|
||||
c.netConn.Close()
|
||||
c.netConn = nil
|
||||
}
|
||||
c.client = nil
|
||||
c.netConnMu.Lock()
|
||||
c.netConn = nil
|
||||
c.netConnMu.Unlock()
|
||||
}
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/derp"
|
||||
@@ -12,11 +13,11 @@ import (
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") != "WebSocket" {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Upgrade", "WebSocket")
|
||||
w.Header().Set("Upgrade", "DERP")
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
|
||||
@@ -27,9 +28,10 @@ func Handler(s *derp.Server) http.Handler {
|
||||
}
|
||||
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
|
||||
}
|
||||
s.Accept(netConn, conn)
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,8 +93,7 @@ func TestSendRecv(t *testing.T) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
b := make([]byte, 1<<16)
|
||||
m, err := c.Recv(b)
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Logf("client%d: %v", i, err)
|
||||
break
|
||||
@@ -103,8 +102,10 @@ func TestSendRecv(t *testing.T) {
|
||||
default:
|
||||
t.Errorf("unexpected message type %T", m)
|
||||
continue
|
||||
case derp.PeerGoneMessage:
|
||||
// Ignore.
|
||||
case derp.ReceivedPacket:
|
||||
recvChs[i] <- []byte(m)
|
||||
recvChs[i] <- append([]byte(nil), m.Data...)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
derp/derpmap/derpmap.go
Normal file
65
derp/derpmap/derpmap.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 derpmap contains information about Tailscale.com's production DERP nodes.
|
||||
package derpmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
|
||||
return &tailcfg.DERPNode{
|
||||
Name: suffix, // updated later
|
||||
RegionID: 0, // updated later
|
||||
IPv4: v4,
|
||||
IPv6: v6,
|
||||
}
|
||||
}
|
||||
|
||||
func derpRegion(id int, code string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
region := &tailcfg.DERPRegion{
|
||||
RegionID: id,
|
||||
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
|
||||
}
|
||||
|
||||
// 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",
|
||||
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
|
||||
),
|
||||
2: derpRegion(2, "sfo",
|
||||
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
|
||||
),
|
||||
3: derpRegion(3, "sin",
|
||||
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
|
||||
),
|
||||
4: derpRegion(4, "fra",
|
||||
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
|
||||
),
|
||||
5: derpRegion(5, "syd",
|
||||
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
29
go.mod
29
go.mod
@@ -1,29 +1,38 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
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-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/goreleaser/nfpm v1.1.10
|
||||
github.com/klauspost/compress v1.9.8
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mdlayher/netlink v1.1.0
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200224122332-ad79bbddc844
|
||||
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678
|
||||
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-20200724155040-d554a2a5e7e1
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
gortc.io/stun v1.22.1
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 // indirect
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
|
||||
86
go.sum
86
go.sum
@@ -19,6 +19,8 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
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=
|
||||
@@ -28,6 +30,8 @@ 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-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=
|
||||
@@ -47,11 +51,13 @@ github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+
|
||||
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
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/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/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
@@ -59,10 +65,15 @@ github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE
|
||||
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/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=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
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/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=
|
||||
@@ -73,54 +84,46 @@ github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:
|
||||
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/hujson v0.0.0-20190930033718-5098e564d9b3 h1:rdtXEo9yffOjh4vZQJw3heaY+ggXKp+zvMX5fihh6lI=
|
||||
github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f h1:q2ynfOHxHaaMnkZ1YHswWeO6wEk7IyOnkFozytZ1ztc=
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20191108062213-b93cdd0582db h1:oP0crfwOb3WZSVrMVm/o51NXN2JirDlcdlNEIPTmgI0=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200207221558-a158079b156a h1:5TWA3nl2QUfL9OiE3tlBpqJd4GYd4hbGtDNkWQQ2fyc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200207221558-a158079b156a/go.mod h1:QPS8HjBzzAXoQNndUNx2efJaQbCCz8nI2Cv1ksTUHyY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200208161837-3cd0a483944a h1:vIyObUBvnXB1XTKTBM4AgoUFR9RHiz/kslGHClkXQVg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200208161837-3cd0a483944a/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200208214841-2981baf46731 h1:sNmny/5pHqHdm081Fx8rcNFnwt0zTGuee/0+Jz+tXCA=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200208214841-2981baf46731/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200211020303-f39bc8eeee1b h1:99LOgoPy0PcmT+yg1HiPJXk/aV/KWeGGh4WtSVB7noM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200211020303-f39bc8eeee1b/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200213180345-a7c4b7719b1d h1:LVJovgZxbmPxtY6kJm4vwMtk0HpcNeI+vU2jB3T8M40=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200213180345-a7c4b7719b1d/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200219153015-ff5ce9e55c86 h1:yVWApjTol2CR3dVbiiSOI78KnElu3AlHFvntYM2egBU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200219153015-ff5ce9e55c86/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200224122332-ad79bbddc844 h1:CChfZok8JbY2dT+BNLHAcCLcGuCTBEhrkYtUwOk5rVY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200224122332-ad79bbddc844/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||
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/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-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
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-20200206161412-a0c6ece9d31a h1:aczoJ0HPNE92XKa7DrIzkNN6esOKO2TBwiiYoKcINhA=
|
||||
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y=
|
||||
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 h1:wCWoJcFExDgyYx2m2hpHgwz8W3+FPdfldvIgzqDIhyg=
|
||||
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/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/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/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-20190923162816-aa69164e4478/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-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/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-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20190911185100-cd5d95a43a6e/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=
|
||||
@@ -128,38 +131,41 @@ golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/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/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/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/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
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-20180628173108-788fd7840127/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-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=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c h1:si3Owrfem175Ry6gKqnh59eOXxDojyBTIHxUKuvK/Eo=
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -1,58 +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 interfaces contains helpers for looking up system network interfaces.
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func Tailscale() (net.IP, *net.Interface, error) {
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, iface := range ifs {
|
||||
if !maybeTailscaleInterfaceName(iface.Name) {
|
||||
continue
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
|
||||
return ipnet.IP, &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil, 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") ||
|
||||
strings.HasPrefix(s, "ts") ||
|
||||
strings.HasPrefix(s, "tailscale")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var cgNAT = func() *net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR("100.64.0.0/10")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipNet
|
||||
}()
|
||||
93
internal/deepprint/deepprint.go
Normal file
93
internal/deepprint/deepprint.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
70
internal/deepprint/deepprint_test.go
Normal file
70
internal/deepprint/deepprint_test.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 deepprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
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: []wgcfg.CIDR{{Mask: 5, IP: wgcfg.IP{Addr: [16]byte{3: 3}}}},
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: []wgcfg.Endpoint{
|
||||
{
|
||||
Host: "foo",
|
||||
Port: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
DNSConfig: router.DNSConfig{
|
||||
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
|
||||
}
|
||||
@@ -5,11 +5,15 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -24,31 +28,48 @@ 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]
|
||||
}
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
type EngineStatus struct {
|
||||
RBytes, WBytes wgengine.ByteCount
|
||||
NumLive int
|
||||
LiveDERPs int // number of active DERP connections
|
||||
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
|
||||
// that they have not changed.
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
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
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
_ 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 *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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||
@@ -82,6 +103,9 @@ type Options struct {
|
||||
// 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
|
||||
// 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
|
||||
@@ -92,27 +116,41 @@ 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
|
||||
// (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale
|
||||
// backend (e.g. cmd/tailscaled) running on the same machine.
|
||||
// (It has nothing to do with the interface between the backends
|
||||
// and the cloud control plane.)
|
||||
type Backend interface {
|
||||
// Start or restart the backend, because a new Handle has connected.
|
||||
Start(opts Options) error
|
||||
// Start starts or restarts the backend, typically when a
|
||||
// frontend client connects.
|
||||
Start(Options) error
|
||||
// StartLoginInteractive requests to start a new interactive login
|
||||
// flow. This should trigger a new BrowseToURL notification
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Logout terminates the current login session and stop the
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *oauth2.Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
// SetPrefs install a new set of user preferences, including
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// SetPrefs installs a new set of user preferences, including
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(new *Prefs)
|
||||
SetPrefs(*Prefs)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
// polling.
|
||||
RequestEngineStatus()
|
||||
// RequestStatus requests that a full Status update
|
||||
// notification is sent.
|
||||
RequestStatus()
|
||||
// FakeExpireAfter pretends that the current key is going to
|
||||
// expire after duration x. This is useful for testing GUIs to
|
||||
// make sure they react properly with keys that are going to
|
||||
|
||||
205
ipn/e2e_test.go
205
ipn/e2e_test.go
@@ -1,205 +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/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/testy"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.io/control" // not yet released
|
||||
)
|
||||
|
||||
func TestIPN(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
|
||||
// Turn off STUN for the test to make it hermitic.
|
||||
// TODO(crawshaw): add a test that runs against a local STUN server.
|
||||
origDefaultSTUN := magicsock.DefaultSTUN
|
||||
magicsock.DefaultSTUN = nil
|
||||
defer func() {
|
||||
magicsock.DefaultSTUN = origDefaultSTUN
|
||||
}()
|
||||
|
||||
// TODO(apenwarr): Make resource checks actually pass.
|
||||
// They don't right now, because (at least) wgengine doesn't fully
|
||||
// shut down.
|
||||
// rc := testy.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, serverURL, true)
|
||||
if err != nil {
|
||||
t.Fatalf("create control server: %v\n", ctl)
|
||||
}
|
||||
|
||||
n1 := newNode(t, "n1", https)
|
||||
defer n1.Backend.Shutdown()
|
||||
n1.Backend.StartLoginInteractive()
|
||||
|
||||
n2 := newNode(t, "n2", https)
|
||||
defer n2.Backend.Shutdown()
|
||||
n2.Backend.StartLoginInteractive()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
t.Skip("skipping ping tests, they are flaky") // TODO(crawshaw): this exposes a real bug!
|
||||
|
||||
n1addr := n1.Backend.NetMap().Addresses[0].IP
|
||||
n2addr := n2.Backend.NetMap().Addresses[0].IP
|
||||
t.Run("ping n2", func(t *testing.T) {
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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) 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...)
|
||||
}
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
e1, err := wgengine.NewUserspaceEngineAdvanced(logfe, tun.TUN(), wgengine.NewFakeRouter, 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@tailscale.com",
|
||||
}
|
||||
n.Start(Options{
|
||||
FrontendLogID: prefix + "-f",
|
||||
Prefs: &Prefs{
|
||||
ControlURL: https.URL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
Persist: &c,
|
||||
},
|
||||
Notify: func(n Notify) {
|
||||
// Automatically visit auth URLs
|
||||
if n.BrowseToURL != nil {
|
||||
t.Logf("\n\n\nURL! %vv\n", *n.BrowseToURL)
|
||||
hc := https.Client()
|
||||
_, err := hc.Get(*n.BrowseToURL)
|
||||
if 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))
|
||||
}
|
||||
@@ -7,6 +7,10 @@ package ipn
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
type FakeBackend struct {
|
||||
@@ -39,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{}})
|
||||
@@ -58,7 +70,7 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
panic("FakeBackend.SetPrefs got nil prefs")
|
||||
}
|
||||
|
||||
b.notify(Notify{Prefs: new.Copy()})
|
||||
b.notify(Notify{Prefs: new.Clone()})
|
||||
if new.WantRunning && !b.live {
|
||||
b.newState(Starting)
|
||||
b.newState(Running)
|
||||
@@ -71,6 +83,10 @@ func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||
func (b *FakeBackend) RequestStatus() {
|
||||
b.notify(Notify{Status: &ipnstate.Status{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
}
|
||||
@@ -9,18 +9,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
frontendLogID string
|
||||
b Backend
|
||||
xnotify func(n Notify)
|
||||
xnotify func(Notify)
|
||||
logf logger.Logf
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
netmapCache *NetworkMap
|
||||
netmapCache *controlclient.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
@@ -47,7 +49,7 @@ func (h *Handle) Start(opts Options) error {
|
||||
h.engineStatusCache = EngineStatus{}
|
||||
h.stateCache = NoState
|
||||
if opts.Prefs != nil {
|
||||
h.prefsCache = opts.Prefs.Copy()
|
||||
h.prefsCache = opts.Prefs.Clone()
|
||||
}
|
||||
xopts := opts
|
||||
xopts.Notify = h.notify
|
||||
@@ -62,14 +64,14 @@ func (h *Handle) Reset() {
|
||||
func (h *Handle) notify(n Notify) {
|
||||
h.mu.Lock()
|
||||
if n.BackendLogID != nil {
|
||||
h.logf("Handle: logs: be:%v fe:%v\n",
|
||||
h.logf("Handle: logs: be:%v fe:%v",
|
||||
*n.BackendLogID, h.frontendLogID)
|
||||
}
|
||||
if n.State != nil {
|
||||
h.stateCache = *n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
h.prefsCache = n.Prefs.Copy()
|
||||
h.prefsCache = n.Prefs.Clone()
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
h.netmapCache = n.NetMap
|
||||
@@ -89,14 +91,14 @@ func (h *Handle) Prefs() *Prefs {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.prefsCache.Copy()
|
||||
return h.prefsCache.Clone()
|
||||
}
|
||||
|
||||
func (h *Handle) UpdatePrefs(updateFn func(p *Prefs)) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
new := h.prefsCache.Copy()
|
||||
new := h.prefsCache.Clone()
|
||||
updateFn(new)
|
||||
h.prefsCache = new
|
||||
h.b.SetPrefs(new)
|
||||
@@ -127,7 +129,7 @@ func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
return []wgcfg.CIDR{}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -161,6 +167,10 @@ func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestStatus() {
|
||||
h.b.RequestStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
||||
h.b.FakeExpireAfter(x)
|
||||
}
|
||||
|
||||
@@ -10,20 +10,19 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -34,15 +33,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
|
||||
@@ -52,43 +55,150 @@ 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.
|
||||
//
|
||||
// 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
|
||||
// to register a debug handler.
|
||||
DebugMux *http.ServeMux
|
||||
|
||||
// ErrorMessage, if not empty, signals that the server will exist
|
||||
// only to relay the provided critical error message to the user.
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
func pump(logf logger.Logf, ctx context.Context, bs *ipn.BackendServer, s net.Conn) {
|
||||
defer logf("Control connection done.\n")
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
resetOnZero bool // call bs.Reset on transition from 1->0 connections
|
||||
|
||||
for ctx.Err() == nil && !bs.GotQuit {
|
||||
msg, err := ipn.ReadMsg(s)
|
||||
bsMu sync.Mutex // lock order: bsMu, then mu
|
||||
bs *ipn.BackendServer
|
||||
|
||||
mu sync.Mutex
|
||||
clients map[net.Conn]bool
|
||||
}
|
||||
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
s.addConn(c)
|
||||
logf("incoming control connection")
|
||||
defer s.removeAndCloseConn(c)
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(c)
|
||||
if err != nil {
|
||||
logf("ReadMsg: %v\n", err)
|
||||
break
|
||||
if ctx.Err() == nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = bs.GotCommandMsg(msg)
|
||||
if err != nil {
|
||||
logf("GotCommandMsg: %v\n", err)
|
||||
break
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) error {
|
||||
bo := backoff.Backoff{Name: "ipnserver"}
|
||||
func (s *server) addConn(c net.Conn) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.clients == nil {
|
||||
s.clients = map[net.Conn]bool{}
|
||||
}
|
||||
s.clients[c] = true
|
||||
}
|
||||
|
||||
func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
s.mu.Lock()
|
||||
delete(s.clients, c)
|
||||
remain := len(s.clients)
|
||||
s.mu.Unlock()
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *server) writeToClients(b []byte) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) 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{
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
}
|
||||
|
||||
// When the context is closed or when we return, whichever is first, close our listner
|
||||
// and all open connections.
|
||||
go func() {
|
||||
<-rctx.Done()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-runDone:
|
||||
}
|
||||
server.stopAll()
|
||||
listen.Close()
|
||||
}()
|
||||
logf("Listening on %v\n", listen.Addr())
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf)
|
||||
|
||||
if opts.ErrorMessage != "" {
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
s, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
serverToClient := func(b []byte) {
|
||||
ipn.WriteMsg(s, b)
|
||||
}
|
||||
go func() {
|
||||
defer s.Close()
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(opts.ErrorMessage)
|
||||
s.Read(make([]byte, 1))
|
||||
}()
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
@@ -104,22 +214,24 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
|
||||
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)
|
||||
})
|
||||
b.SetCmpDiff(func(x, y interface{}) string { return cmp.Diff(x, y) })
|
||||
|
||||
var s net.Conn
|
||||
serverToClient := func(b []byte) {
|
||||
if s != nil {
|
||||
ipn.WriteMsg(s, b)
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
bs := ipn.NewBackendServer(logf, b, serverToClient)
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
bs.GotCommand(&ipn.Command{
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
Version: version.LONG,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
@@ -130,57 +242,18 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
|
||||
})
|
||||
}
|
||||
|
||||
var oldS net.Conn
|
||||
//lint:ignore SA4006 ctx is never used, but has to be defined so
|
||||
// that it can be assigned to in the following for loop. It's a
|
||||
// bit of necessary code convolution to work around Go's variable
|
||||
// shadowing rules.
|
||||
ctx, cancel := context.WithCancel(rctx)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; rctx.Err() == nil; i++ {
|
||||
s, err = listen.Accept()
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v\n", 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.\n", i)
|
||||
stopAll()
|
||||
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
oldS = s
|
||||
|
||||
go func(ctx context.Context, bs *ipn.BackendServer, s net.Conn, i int) {
|
||||
si := fmt.Sprintf("%d: ", i)
|
||||
pump(func(fmt string, args ...interface{}) {
|
||||
logf(si+fmt, args...)
|
||||
}, ctx, bs, s)
|
||||
if !opts.SurviveDisconnects || bs.GotQuit {
|
||||
bs.Reset()
|
||||
s.Close()
|
||||
}
|
||||
// Quitting not allowed, just keep going.
|
||||
bs.GotQuit = false
|
||||
}(ctx, bs, 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()
|
||||
}
|
||||
|
||||
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
@@ -202,10 +275,10 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
var sig os.Signal
|
||||
select {
|
||||
case sig = <-interrupt:
|
||||
logf("BabysitProc: got signal: %v\n", sig)
|
||||
logf("BabysitProc: got signal: %v", sig)
|
||||
close(done)
|
||||
case <-ctx.Done():
|
||||
logf("BabysitProc: context done\n")
|
||||
logf("BabysitProc: context done")
|
||||
sig = os.Kill
|
||||
close(done)
|
||||
}
|
||||
@@ -215,11 +288,11 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
proc.mu.Unlock()
|
||||
}()
|
||||
|
||||
bo := backoff.Backoff{Name: "BabysitProc"}
|
||||
bo := backoff.NewBackoff("BabysitProc", logf)
|
||||
|
||||
for {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v\n", executable, args)
|
||||
log.Printf("exec: %#v %v", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
@@ -230,7 +303,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
// We never need to actually write to wStdin.
|
||||
rStdin, wStdin, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 1: %v\n", err)
|
||||
log.Printf("os.Pipe 1: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -242,7 +315,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
// logf() calls. bufio is more appropriate.
|
||||
rStdout, wStdout, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 2: %v\n", err)
|
||||
log.Printf("os.Pipe 2: %v", err)
|
||||
}
|
||||
go func(r *os.File) {
|
||||
defer r.Close()
|
||||
@@ -250,7 +323,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
for {
|
||||
s, err := rb.ReadString('\n')
|
||||
if s != "" {
|
||||
logf("%s\n", strings.TrimSuffix(s, "\n"))
|
||||
logf("%s", s)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
|
||||
77
ipn/ipnserver/server_test.go
Normal file
77
ipn/ipnserver/server_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 ipnserver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
func TestRunMultipleAccepts(t *testing.T) {
|
||||
t.Skipf("TODO(bradfitz): finish this test, once other fires are out")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
td, err := ioutil.TempDir("", "TestRunMultipleAccepts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
socketPath := filepath.Join(td, "tailscale.sock")
|
||||
|
||||
logf := func(format string, args ...interface{}) {
|
||||
format = strings.TrimRight(format, "\n")
|
||||
println(fmt.Sprintf(format, args...))
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
|
||||
connect := func() {
|
||||
for i := 1; i <= 2; i++ {
|
||||
logf("connect %d ...", i)
|
||||
c, err := safesocket.Connect(socketPath, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
bc := ipn.NewBackendClient(logf, clientToServer)
|
||||
prefs := ipn.NewPrefs()
|
||||
bc.SetPrefs(prefs)
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
logTriggerTestf := func(format string, args ...interface{}) {
|
||||
logf(format, args...)
|
||||
if strings.HasPrefix(format, "Listening on ") {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer eng.Close()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: socketPath,
|
||||
}
|
||||
t.Logf("pre-Run")
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", opts, eng)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
304
ipn/ipnstate/ipnstate.go
Normal file
304
ipn/ipnstate/ipnstate.go
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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 ipnstate captures the entire state of the Tailscale network.
|
||||
//
|
||||
// It's a leaf package so ipn, wgengine, and magicsock can all depend on it.
|
||||
package ipnstate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.Public {
|
||||
kk := make([]key.Public, 0, len(s.Peer))
|
||||
for k := range s.Peer {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Slice(kk, func(i, j int) bool { return bytes.Compare(kk[i][:], kk[j][:]) < 0 })
|
||||
return kk
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
TailAddr string // Tailscale IP
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// InMagicSock means that this peer is being tracked by magicsock.
|
||||
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
|
||||
InMagicSock bool
|
||||
|
||||
// InEngine means that this peer is tracked by the wireguard engine.
|
||||
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
|
||||
InEngine bool
|
||||
}
|
||||
|
||||
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
|
||||
func (ps *PeerStatus) SimpleHostName() string {
|
||||
n := ps.HostName
|
||||
n = strings.TrimSuffix(n, ".local")
|
||||
n = strings.TrimSuffix(n, ".localdomain")
|
||||
return n
|
||||
}
|
||||
|
||||
type StatusBuilder struct {
|
||||
mu sync.Mutex
|
||||
locked bool
|
||||
st Status
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.locked = true
|
||||
return &sb.st
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
if sb.locked {
|
||||
log.Printf("[unexpected] ipnstate: AddUser after Locked")
|
||||
return
|
||||
}
|
||||
|
||||
if sb.st.User == nil {
|
||||
sb.st.User = make(map[tailcfg.UserID]tailcfg.UserProfile)
|
||||
}
|
||||
|
||||
sb.st.User[id] = up
|
||||
}
|
||||
|
||||
// AddPeer adds a peer node to the status.
|
||||
//
|
||||
// Its PeerStatus is mixed with any previous status already added.
|
||||
func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st == nil {
|
||||
panic("nil PeerStatus")
|
||||
}
|
||||
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
if sb.locked {
|
||||
log.Printf("[unexpected] ipnstate: AddPeer after Locked")
|
||||
return
|
||||
}
|
||||
|
||||
if sb.st.Peer == nil {
|
||||
sb.st.Peer = make(map[key.Public]*PeerStatus)
|
||||
}
|
||||
e, ok := sb.st.Peer[peer]
|
||||
if !ok {
|
||||
sb.st.Peer[peer] = st
|
||||
st.PublicKey = peer
|
||||
return
|
||||
}
|
||||
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
if v := st.Relay; v != "" {
|
||||
e.Relay = v
|
||||
}
|
||||
if v := st.UserID; v != 0 {
|
||||
e.UserID = v
|
||||
}
|
||||
if v := st.TailAddr; v != "" {
|
||||
e.TailAddr = v
|
||||
}
|
||||
if v := st.OS; v != "" {
|
||||
e.OS = st.OS
|
||||
}
|
||||
if v := st.Addrs; v != nil {
|
||||
e.Addrs = v
|
||||
}
|
||||
if v := st.CurAddr; v != "" {
|
||||
e.CurAddr = v
|
||||
}
|
||||
if v := st.RxBytes; v != 0 {
|
||||
e.RxBytes = v
|
||||
}
|
||||
if v := st.TxBytes; v != 0 {
|
||||
e.TxBytes = v
|
||||
}
|
||||
if v := st.LastHandshake; !v.IsZero() {
|
||||
e.LastHandshake = v
|
||||
}
|
||||
if v := st.Created; !v.IsZero() {
|
||||
e.Created = v
|
||||
}
|
||||
if v := st.LastSeen; !v.IsZero() {
|
||||
e.LastSeen = v
|
||||
}
|
||||
if v := st.LastWrite; !v.IsZero() {
|
||||
e.LastWrite = v
|
||||
}
|
||||
if st.InNetworkMap {
|
||||
e.InNetworkMap = true
|
||||
}
|
||||
if st.InMagicSock {
|
||||
e.InMagicSock = true
|
||||
}
|
||||
if st.InEngine {
|
||||
e.InEngine = true
|
||||
}
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
UpdateStatus(*StatusBuilder)
|
||||
}
|
||||
|
||||
func (st *Status) WriteHTML(w io.Writer) {
|
||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
||||
|
||||
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>\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()
|
||||
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
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
|
||||
if up, ok := st.User[ps.UserID]; ok {
|
||||
owner = up.LoginName
|
||||
if i := strings.Index(owner, "@"); i != -1 {
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
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(),
|
||||
html.EscapeString(ps.SimpleHostName()),
|
||||
osEmoji(ps.OS),
|
||||
ps.TailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
actAgo,
|
||||
)
|
||||
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>", addr)
|
||||
} else {
|
||||
f("%s<br>", addr)
|
||||
}
|
||||
}
|
||||
if ps.CurAddr != "" && !match {
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
|
||||
}
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
}
|
||||
f("</tbody>\n</table>\n")
|
||||
f("</body>\n</html>\n")
|
||||
}
|
||||
|
||||
func osEmoji(os string) string {
|
||||
switch os {
|
||||
case "linux":
|
||||
return "🐧"
|
||||
case "macOS":
|
||||
return "🍎"
|
||||
case "windows":
|
||||
return "🖥️"
|
||||
case "iOS":
|
||||
return "📱"
|
||||
case "android":
|
||||
return "🤖"
|
||||
case "freebsd":
|
||||
return "👿"
|
||||
case "openbsd":
|
||||
return "🐡"
|
||||
}
|
||||
return "👽"
|
||||
}
|
||||
1047
ipn/local.go
1047
ipn/local.go
File diff suppressed because it is too large
Load Diff
105
ipn/message.go
105
ipn/message.go
@@ -13,7 +13,9 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -34,23 +36,33 @@ type FakeExpireAfterArgs struct {
|
||||
// Command is a command message that is JSON encoded and sent by a
|
||||
// frontend to a backend.
|
||||
type Command struct {
|
||||
_ 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
|
||||
RequestEngineStatus *NoArgs
|
||||
RequestStatus *NoArgs
|
||||
FakeExpireAfter *FakeExpireAfterArgs
|
||||
}
|
||||
|
||||
type BackendServer struct {
|
||||
logf logger.Logf
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(b []byte) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(jsonMsg []byte) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
}
|
||||
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
|
||||
@@ -65,25 +77,38 @@ func (bs *BackendServer) send(n Notify) {
|
||||
n.Version = version.LONG
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v\n", err, n)
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
// Inform the BackendServer of an incoming message.
|
||||
func (bs *BackendServer) SendErrorMessage(msg string) {
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
}
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
cmd := Command{}
|
||||
if err := json.Unmarshal(b, &cmd); err != nil {
|
||||
cmd := &Command{}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.GotCommand(&cmd)
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
cmd.Version = version.LONG
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
if cmd.Version != version.LONG {
|
||||
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v\n",
|
||||
if cmd.Version != version.LONG && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
cmd.Version, version.LONG)
|
||||
bs.logf("%s\n", vs)
|
||||
bs.logf("%s", vs)
|
||||
// ignore the command, but send a message back to the
|
||||
// caller so it can realize the version mismatch too.
|
||||
// We don't want to exit because it might cause a crash
|
||||
@@ -105,6 +130,9 @@ 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
|
||||
@@ -114,6 +142,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
@@ -125,16 +156,20 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
func (bs *BackendServer) Reset() error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotCommand(&Command{Logout: &NoArgs{}})
|
||||
return bs.GotFakeCommand(&Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
logf logger.Logf
|
||||
sendCommandMsg func(b []byte)
|
||||
notify func(n Notify)
|
||||
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(b []byte)) *BackendClient {
|
||||
func NewBackendClient(logf logger.Logf, sendCommandMsg func(jsonb []byte)) *BackendClient {
|
||||
return &BackendClient{
|
||||
logf: logf,
|
||||
sendCommandMsg: sendCommandMsg,
|
||||
@@ -142,14 +177,18 @@ func NewBackendClient(logf logger.Logf, sendCommandMsg func(b []byte)) *BackendC
|
||||
}
|
||||
|
||||
func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
if len(b) == 0 {
|
||||
// not interesting
|
||||
return
|
||||
}
|
||||
n := Notify{}
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message")
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
|
||||
}
|
||||
if n.Version != version.LONG {
|
||||
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v",
|
||||
if n.Version != version.LONG && !bc.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
|
||||
version.LONG, n.Version)
|
||||
bc.logf("%s\n", vs)
|
||||
bc.logf("%s", vs)
|
||||
// delete anything in the notification except the version,
|
||||
// to prevent incorrect operation.
|
||||
n = Notify{
|
||||
@@ -171,6 +210,10 @@ func (bc *BackendClient) send(cmd Command) {
|
||||
bc.sendCommandMsg(b)
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) {
|
||||
bc.notify = fn
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Quit() error {
|
||||
bc.send(Command{Quit: &NoArgs{}})
|
||||
return nil
|
||||
@@ -187,6 +230,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{}})
|
||||
}
|
||||
@@ -199,11 +246,16 @@ func (bc *BackendClient) RequestEngineStatus() {
|
||||
bc.send(Command{RequestEngineStatus: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) RequestStatus() {
|
||||
bc.send(Command{AllowVersionSkew: true, RequestStatus: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||
}
|
||||
|
||||
const MSG_MAX = 1024 * 1024
|
||||
// MaxMessageSize is the maximum message size, in bytes.
|
||||
const MaxMessageSize = 1 << 20
|
||||
|
||||
// TODO(apenwarr): incremental json decode?
|
||||
// That would let us avoid storing the whole byte array uselessly in RAM.
|
||||
@@ -214,14 +266,17 @@ func ReadMsg(r io.Reader) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
n := binary.LittleEndian.Uint32(cb)
|
||||
if n > 1024*1024 {
|
||||
if n > MaxMessageSize {
|
||||
return nil, fmt.Errorf("ipn.Read: message too large: %v bytes", n)
|
||||
}
|
||||
b := make([]byte, n)
|
||||
_, err = io.ReadFull(r, b)
|
||||
nn, err := io.ReadFull(r, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nn != int(n) {
|
||||
return nil, fmt.Errorf("ipn.Read: expected %v bytes, got %v", n, nn)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -229,8 +284,12 @@ func ReadMsg(r io.Reader) ([]byte, error) {
|
||||
// That would save RAM, at the expense of having to encode once so that
|
||||
// we can produce the initial byte count.
|
||||
func WriteMsg(w io.Writer, b []byte) error {
|
||||
// TODO(bradfitz): this does two writes to w, which likely
|
||||
// does two writes on the wire, two frame generations, etc. We
|
||||
// should take a concrete buffered type, or use a sync.Pool to
|
||||
// allocate a buf and do one write.
|
||||
cb := make([]byte, 4)
|
||||
if len(b) > MSG_MAX {
|
||||
if len(b) > MaxMessageSize {
|
||||
return fmt.Errorf("ipn.Write: message too large: %v bytes", len(b))
|
||||
}
|
||||
binary.LittleEndian.PutUint32(cb, uint32(len(b)))
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/testy"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestReadWrite(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := testy.NewResourceCheck()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
@@ -62,10 +62,9 @@ func TestReadWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := testy.NewResourceCheck()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
42
ipn/policy/policy.go
Normal file
42
ipn/policy/policy.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 policy contains various policy decisions that need to be
|
||||
// shared between the node client & control server.
|
||||
package policy
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
// IsInterestingService reports whether service s on the given operating
|
||||
// system (a version.OS value) is an interesting enough port to report
|
||||
// to our peer nodes for discovery purposes.
|
||||
func IsInterestingService(s tailcfg.Service, os string) bool {
|
||||
if s.Proto != tailcfg.TCP {
|
||||
return false
|
||||
}
|
||||
if os != "windows" {
|
||||
// For non-Windows machines, assume all TCP listeners
|
||||
// are interesting enough. We don't see listener spam
|
||||
// there.
|
||||
return true
|
||||
}
|
||||
// Windows has tons of TCP listeners. We need to move to a blacklist
|
||||
// model later, but for now we just whitelist some common ones:
|
||||
switch s.Port {
|
||||
case 22, // ssh
|
||||
80, // http
|
||||
443, // https (but no hostname, so little useless)
|
||||
3389, // rdp
|
||||
5900, // vnc
|
||||
32400, // plex
|
||||
|
||||
// And now some arbitary HTTP dev server ports:
|
||||
// Eventually we'll remove this and make all ports
|
||||
// work, once we nicely filter away noisy system
|
||||
// ports.
|
||||
8000, 8080, 8443, 8888:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user