Compare commits
618 Commits
aaron/migr
...
tsweb/clie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e76660843 | ||
|
|
9425312923 | ||
|
|
49896cbdfa | ||
|
|
c56e94af2d | ||
|
|
a3f11e7710 | ||
|
|
10acc06389 | ||
|
|
a17c45fd6e | ||
|
|
a8e32f1a4b | ||
|
|
371e1ebf07 | ||
|
|
eb6883bb5a | ||
|
|
37925b3e7a | ||
|
|
301e59f398 | ||
|
|
ab7749aed7 | ||
|
|
f57cc19ba2 | ||
|
|
b4c1f039b6 | ||
|
|
c3b979a176 | ||
|
|
34bfd7b419 | ||
|
|
66e46bf501 | ||
|
|
6d65c04987 | ||
|
|
767e839db5 | ||
|
|
7adf15f90e | ||
|
|
ec9213a627 | ||
|
|
eef15b4ffc | ||
|
|
ed46442cb1 | ||
|
|
5ebb271322 | ||
|
|
058d427fa6 | ||
|
|
68f8e5678e | ||
|
|
0554deb48c | ||
|
|
6114247d0a | ||
|
|
52212f4323 | ||
|
|
90a7d3066c | ||
|
|
2315bf246a | ||
|
|
c1ecae13ab | ||
|
|
aa37be70cf | ||
|
|
35bdbeda9f | ||
|
|
9d89e85db7 | ||
|
|
84777354a0 | ||
|
|
9a76deb4b0 | ||
|
|
cde37f5307 | ||
|
|
f7016d8c00 | ||
|
|
c2831f6614 | ||
|
|
9edb848505 | ||
|
|
1ecc16da5f | ||
|
|
306deea03a | ||
|
|
6afffece8a | ||
|
|
4f14ed2ad6 | ||
|
|
f1cd67488d | ||
|
|
44ad7b3746 | ||
|
|
125b982ba5 | ||
|
|
b76d8a88ae | ||
|
|
b242e2c2cb | ||
|
|
8478358d77 | ||
|
|
de5c6ed4be | ||
|
|
736a44264f | ||
|
|
1e6f0bb608 | ||
|
|
aaca911904 | ||
|
|
b145a22f55 | ||
|
|
9cc3f7a3d6 | ||
|
|
ac657caaf1 | ||
|
|
fcf4d044fa | ||
|
|
486195edf0 | ||
|
|
45b5d0983c | ||
|
|
4c05d43008 | ||
|
|
894b237a70 | ||
|
|
f1cc8ab3f9 | ||
|
|
2a6c237d4c | ||
|
|
453620dca1 | ||
|
|
41db1d7bba | ||
|
|
907c56c200 | ||
|
|
e1bcecc393 | ||
|
|
bb4b35e923 | ||
|
|
88cc0ad9f7 | ||
|
|
7560435eb5 | ||
|
|
32d486e2bf | ||
|
|
3c53bedbbf | ||
|
|
388b124513 | ||
|
|
efd6d90dd7 | ||
|
|
3f6b0d8c84 | ||
|
|
bec9815f02 | ||
|
|
486ab427b4 | ||
|
|
7c04846eac | ||
|
|
9ab70212f4 | ||
|
|
6b56e92acc | ||
|
|
a3c7b21cd1 | ||
|
|
abcb7ec1ce | ||
|
|
2c782d742c | ||
|
|
24f0e91169 | ||
|
|
1138f4eb5f | ||
|
|
9b5e29761c | ||
|
|
8bdc03913c | ||
|
|
3304819739 | ||
|
|
9101fabdf8 | ||
|
|
94a51bdd62 | ||
|
|
f8b0caa8c2 | ||
|
|
c19b5bfbc3 | ||
|
|
0573f6e953 | ||
|
|
60e5761d60 | ||
|
|
7aba0b0d78 | ||
|
|
7a82fd8dbe | ||
|
|
354885a08d | ||
|
|
4f95b6966b | ||
|
|
c95de4c7a8 | ||
|
|
3d70fecde4 | ||
|
|
96d7af3469 | ||
|
|
8cda647a0f | ||
|
|
49015b00fe | ||
|
|
2bbedd2001 | ||
|
|
60ab8089ff | ||
|
|
cd313e410b | ||
|
|
8c0572e088 | ||
|
|
a7648a6723 | ||
|
|
ffaa6be8a4 | ||
|
|
7b1c3dfd28 | ||
|
|
f05a9f3e7f | ||
|
|
339397ab74 | ||
|
|
9d1a3a995c | ||
|
|
92fb80d55f | ||
|
|
28ee355c56 | ||
|
|
cd4c71c122 | ||
|
|
fd8c8a3700 | ||
|
|
3f1f906b63 | ||
|
|
cb53846717 | ||
|
|
0c427f23bd | ||
|
|
4d94d72fba | ||
|
|
0a86705d59 | ||
|
|
a795b4a641 | ||
|
|
6ebd87c669 | ||
|
|
1ca5dcce15 | ||
|
|
2e4e7d6b9d | ||
|
|
79ee6d6e1e | ||
|
|
2e19790f61 | ||
|
|
e42be5a060 | ||
|
|
075abd8ec1 | ||
|
|
12a2221db2 | ||
|
|
97ee0bc685 | ||
|
|
b0a984dc26 | ||
|
|
626f650033 | ||
|
|
d4413f723d | ||
|
|
cafd9a2bec | ||
|
|
ab310a7f60 | ||
|
|
d9eca20ee2 | ||
|
|
243ce6ccc1 | ||
|
|
9c64e015e5 | ||
|
|
832f1028c7 | ||
|
|
a874f1afd8 | ||
|
|
e26376194d | ||
|
|
77f56794c9 | ||
|
|
1377618dbc | ||
|
|
8e840489ed | ||
|
|
2cf6e12790 | ||
|
|
c11af12a49 | ||
|
|
ba41d14320 | ||
|
|
1f57088cbd | ||
|
|
3417ddc00c | ||
|
|
2a9817da39 | ||
|
|
bfe5623a86 | ||
|
|
4a58b1c293 | ||
|
|
7c1068b7ac | ||
|
|
fbacc0bd39 | ||
|
|
8b80d63b42 | ||
|
|
61886e031e | ||
|
|
d4de60c3ae | ||
|
|
30d9201a11 | ||
|
|
32b8f25ed1 | ||
|
|
6829caf6de | ||
|
|
e48c0bf0e7 | ||
|
|
f314fa4a4a | ||
|
|
dc5bc32d8f | ||
|
|
6697690b55 | ||
|
|
a2153afeeb | ||
|
|
0f5090c526 | ||
|
|
88097b836a | ||
|
|
2ae670eb71 | ||
|
|
0ed088b47b | ||
|
|
909e9eabe4 | ||
|
|
b6d20e6f8f | ||
|
|
1302295299 | ||
|
|
c6794dec11 | ||
|
|
c783f28228 | ||
|
|
c1cbd41fdc | ||
|
|
e1cdcf7708 | ||
|
|
80692edcb8 | ||
|
|
27a0f0a55b | ||
|
|
99f17a7135 | ||
|
|
4dda949760 | ||
|
|
a076213f58 | ||
|
|
4451a7c364 | ||
|
|
fe95d81b43 | ||
|
|
5b110685fb | ||
|
|
0b3b81b37a | ||
|
|
6172f9590b | ||
|
|
1543e233e6 | ||
|
|
167e154bcc | ||
|
|
67e912824a | ||
|
|
63b1a4e35d | ||
|
|
f077b672e4 | ||
|
|
2e0aa151c9 | ||
|
|
62130e6b68 | ||
|
|
2a9d46c38f | ||
|
|
eefee6f149 | ||
|
|
699996ad6c | ||
|
|
12f8c98823 | ||
|
|
1c4a047ad0 | ||
|
|
f8f0b981ac | ||
|
|
a353ae079b | ||
|
|
43e230d4cd | ||
|
|
5dd0b02133 | ||
|
|
d3c8c3dd00 | ||
|
|
64f16f7f38 | ||
|
|
6554a0cbec | ||
|
|
d17312265e | ||
|
|
4321d1d6e9 | ||
|
|
2492ca2900 | ||
|
|
570cb018da | ||
|
|
dc1d8826a2 | ||
|
|
67882ad35d | ||
|
|
07eacdfe92 | ||
|
|
d06fac0ede | ||
|
|
9d09c821f7 | ||
|
|
2aa8299c37 | ||
|
|
88ee857bc8 | ||
|
|
1a691ec5b2 | ||
|
|
6a156f6243 | ||
|
|
525b9c806f | ||
|
|
fc5b137d25 | ||
|
|
32e0ba5e68 | ||
|
|
399a80785e | ||
|
|
c0b4a54146 | ||
|
|
c4fe9c536d | ||
|
|
370b2c37e0 | ||
|
|
cb94ddb7b8 | ||
|
|
66f97f4bea | ||
|
|
e32e5c0d0c | ||
|
|
3d180a16c3 | ||
|
|
4e86857313 | ||
|
|
745ee97973 | ||
|
|
a4fd4fd845 | ||
|
|
e3cb982139 | ||
|
|
5ae786988c | ||
|
|
0ca8bf1e26 | ||
|
|
03e848e3b5 | ||
|
|
7c88eeba86 | ||
|
|
f0ee03dfaf | ||
|
|
4664318be2 | ||
|
|
678bb92bb8 | ||
|
|
9b6e48658f | ||
|
|
85215ed58a | ||
|
|
b69059334b | ||
|
|
84c99fe0d9 | ||
|
|
da90fab899 | ||
|
|
ca49b29582 | ||
|
|
cb2fd5be92 | ||
|
|
d27a6e1c53 | ||
|
|
4f454f4122 | ||
|
|
ea84fc9ad2 | ||
|
|
1ce08256c0 | ||
|
|
827abbeeaa | ||
|
|
d1ecb1f43b | ||
|
|
a743b66f9d | ||
|
|
58ab66ec51 | ||
|
|
e8b06b2232 | ||
|
|
df8b1b2179 | ||
|
|
4d730e154c | ||
|
|
b9fb8ac702 | ||
|
|
5c38f0979e | ||
|
|
024d48d9c1 | ||
|
|
29ded8f9f9 | ||
|
|
68307c1411 | ||
|
|
2804327074 | ||
|
|
8d3d48e000 | ||
|
|
8864112a0c | ||
|
|
9ed3a061c3 | ||
|
|
6e967446e4 | ||
|
|
0d7303b798 | ||
|
|
d1ce7a9b5e | ||
|
|
5def4f4a1c | ||
|
|
1c6ff310ae | ||
|
|
48605226dd | ||
|
|
f46c1aede0 | ||
|
|
73d128238e | ||
|
|
787fc41fa4 | ||
|
|
5783adcc6f | ||
|
|
503b6dd8be | ||
|
|
9e9ea6e974 | ||
|
|
459744c9ea | ||
|
|
7675d323fa | ||
|
|
270942094f | ||
|
|
be190e990f | ||
|
|
4d7927047c | ||
|
|
ddb4040aa0 | ||
|
|
c1e6888fc7 | ||
|
|
3ae7140690 | ||
|
|
bcf7b63d7e | ||
|
|
c5bf868940 | ||
|
|
42fd964090 | ||
|
|
979d29b5f5 | ||
|
|
1f4a34588b | ||
|
|
a82f275619 | ||
|
|
b3c3a9f174 | ||
|
|
042f82ea32 | ||
|
|
633d08bd7b | ||
|
|
d35ce1add9 | ||
|
|
c3ab36cb9d | ||
|
|
8032b966a1 | ||
|
|
d78b334964 | ||
|
|
161d1d281a | ||
|
|
1145b9751d | ||
|
|
1e876a3c1d | ||
|
|
a8f10c23b2 | ||
|
|
b2b5379348 | ||
|
|
13de36303d | ||
|
|
095d3edd33 | ||
|
|
43819309e1 | ||
|
|
1b8a0dfe5e | ||
|
|
018a382729 | ||
|
|
2e07245384 | ||
|
|
aa87e999dc | ||
|
|
f58751eb2b | ||
|
|
ce11c82d51 | ||
|
|
90ba26cea1 | ||
|
|
7778d708a6 | ||
|
|
f66ddb544c | ||
|
|
e3b2250e26 | ||
|
|
6f521c138d | ||
|
|
04a3118d45 | ||
|
|
c791e64881 | ||
|
|
7330aa593e | ||
|
|
7f17e04a5a | ||
|
|
4722f7e322 | ||
|
|
3ede3aafe4 | ||
|
|
f844791e15 | ||
|
|
cd35a79136 | ||
|
|
f85dc6f97c | ||
|
|
5acc7c4b1e | ||
|
|
c328770184 | ||
|
|
588a234fdc | ||
|
|
c3ef6fb4ee | ||
|
|
85de580455 | ||
|
|
d0906cda97 | ||
|
|
7c386ca6d2 | ||
|
|
7f057d7489 | ||
|
|
c7cea825ae | ||
|
|
280255acae | ||
|
|
ff1b35ec6c | ||
|
|
9a655a1d58 | ||
|
|
28cb1221ba | ||
|
|
d5a870b4dc | ||
|
|
162488a775 | ||
|
|
c5150eae67 | ||
|
|
80b138f0df | ||
|
|
4b49ca4a12 | ||
|
|
10f1c90f4d | ||
|
|
29f7df9d8f | ||
|
|
83c41f3697 | ||
|
|
20f17d6e7b | ||
|
|
bd0c32ca21 | ||
|
|
b7f51a1468 | ||
|
|
f352f8a0e6 | ||
|
|
8dec1a8724 | ||
|
|
4ecc7fdf5f | ||
|
|
6866aaeab3 | ||
|
|
c889254b42 | ||
|
|
228d0c6aea | ||
|
|
64bbf1738e | ||
|
|
a5fd51ebdc | ||
|
|
a7c910e361 | ||
|
|
edb02b63f8 | ||
|
|
782ccb5655 | ||
|
|
bb34589748 | ||
|
|
9e50da321b | ||
|
|
bdc7a61c24 | ||
|
|
33b006cacf | ||
|
|
e5d272f445 | ||
|
|
7c95734907 | ||
|
|
8546ff98fb | ||
|
|
c153e6ae2f | ||
|
|
11e6247d2a | ||
|
|
690446c784 | ||
|
|
cef0a474f8 | ||
|
|
03b2c44a21 | ||
|
|
1bec2cbbd5 | ||
|
|
f571536598 | ||
|
|
e09c434e5d | ||
|
|
e1b71c83ac | ||
|
|
a257b2f88b | ||
|
|
fb18af5564 | ||
|
|
c573bef0aa | ||
|
|
6cfcb3cae4 | ||
|
|
e978299bf0 | ||
|
|
22680a11ae | ||
|
|
75784e10e2 | ||
|
|
6a627e5a33 | ||
|
|
92459a9248 | ||
|
|
7012bf7981 | ||
|
|
07b29f13dc | ||
|
|
f49b9f75b8 | ||
|
|
c0e0a5458f | ||
|
|
81fd00a6b7 | ||
|
|
d42d570066 | ||
|
|
2c0bda6e2e | ||
|
|
3d29da105c | ||
|
|
765d3253f3 | ||
|
|
ba4e58f429 | ||
|
|
7bfb7744b7 | ||
|
|
f475e5550c | ||
|
|
45138fcfba | ||
|
|
b0ed863d55 | ||
|
|
4d1b3bc26f | ||
|
|
6d5c3c1637 | ||
|
|
5a3da3cd7f | ||
|
|
90fd04cbde | ||
|
|
e3cb8cc88d | ||
|
|
8d3acc9235 | ||
|
|
483109b8fc | ||
|
|
59879e5770 | ||
|
|
1bf65e4760 | ||
|
|
38bbb30aaf | ||
|
|
f4da995940 | ||
|
|
02582083d5 | ||
|
|
40fa2a420c | ||
|
|
8ed4fd1dbc | ||
|
|
3b39ca9017 | ||
|
|
e0d291ab8a | ||
|
|
2b00d6922f | ||
|
|
7b4e85aa78 | ||
|
|
e99c7c3ee5 | ||
|
|
38e4d303a2 | ||
|
|
62a1e9a44f | ||
|
|
985535aebc | ||
|
|
d1d5d52b2c | ||
|
|
2522b0615f | ||
|
|
c98652c333 | ||
|
|
524f53de89 | ||
|
|
8c2b755b2e | ||
|
|
a31e43f760 | ||
|
|
c628132b34 | ||
|
|
e04acabfde | ||
|
|
cb960d6cdd | ||
|
|
27e37cf9b3 | ||
|
|
946451b43e | ||
|
|
840d69e1eb | ||
|
|
3ba9f8dd04 | ||
|
|
7c99210e68 | ||
|
|
920ec69241 | ||
|
|
2a933c1903 | ||
|
|
43f7ec48ca | ||
|
|
3177ccabe5 | ||
|
|
7908b6d616 | ||
|
|
ed10a1769b | ||
|
|
5ba57e4661 | ||
|
|
d5abdd915e | ||
|
|
74eb99aed1 | ||
|
|
09d0b632d4 | ||
|
|
d39a5e4417 | ||
|
|
d2fd101eb4 | ||
|
|
8ac5976897 | ||
|
|
7300b908fb | ||
|
|
ca19cf13e9 | ||
|
|
33b359642e | ||
|
|
6f9aed1656 | ||
|
|
4cb1bfee44 | ||
|
|
4a89642f7f | ||
|
|
9e81db50f6 | ||
|
|
8a11f76a0d | ||
|
|
ec90522a53 | ||
|
|
0e203e414f | ||
|
|
0bf8c8e710 | ||
|
|
f6ea6863de | ||
|
|
bb31fd7d1c | ||
|
|
535fad16f8 | ||
|
|
f61b306133 | ||
|
|
583e86b7df | ||
|
|
df89b7de10 | ||
|
|
8a246487c2 | ||
|
|
8765568373 | ||
|
|
9d8b7a7383 | ||
|
|
57a008a1e1 | ||
|
|
13377e6458 | ||
|
|
9de8287d47 | ||
|
|
c350cd1f06 | ||
|
|
f13b8bf0cf | ||
|
|
731688e5cc | ||
|
|
7083246409 | ||
|
|
d92047cc30 | ||
|
|
7a97e64ef0 | ||
|
|
cc3806056f | ||
|
|
916aa782af | ||
|
|
60cd4ac08d | ||
|
|
1b78dc1f33 | ||
|
|
3efd83555f | ||
|
|
812025a39c | ||
|
|
39b289578e | ||
|
|
c9a4dbe383 | ||
|
|
f11c270c6b | ||
|
|
d2dec13392 | ||
|
|
e7a78bc28f | ||
|
|
df02bb013a | ||
|
|
ebc630c6c0 | ||
|
|
ccace1f7df | ||
|
|
e1fb687104 | ||
|
|
654b5a0616 | ||
|
|
50d211d1a4 | ||
|
|
e59dc29a55 | ||
|
|
60a028a4f6 | ||
|
|
927e2e3e7c | ||
|
|
82e067e0ff | ||
|
|
95494a155e | ||
|
|
9534783758 | ||
|
|
f34590d9ed | ||
|
|
c6d96a2b61 | ||
|
|
0498d5ea86 | ||
|
|
1f95bfedf7 | ||
|
|
9526858b1e | ||
|
|
df3996cae3 | ||
|
|
97b6d3e917 | ||
|
|
9ebab961c9 | ||
|
|
6d3490f399 | ||
|
|
51b0169b10 | ||
|
|
b4d3e2928b | ||
|
|
2b892ad6e7 | ||
|
|
6ef2105a8e | ||
|
|
8c4adde083 | ||
|
|
c87782ba9d | ||
|
|
09e0ccf4c2 | ||
|
|
a1d9f65354 | ||
|
|
5e8a80b845 | ||
|
|
558735bc63 | ||
|
|
489e27f085 | ||
|
|
56526ff57f | ||
|
|
09aed46d44 | ||
|
|
223713d4a1 | ||
|
|
83fa17d26c | ||
|
|
958c89470b | ||
|
|
e109cf9fdd | ||
|
|
3ff44b2307 | ||
|
|
ccdd534e81 | ||
|
|
047b324933 | ||
|
|
f0d6228c52 | ||
|
|
920de86cee | ||
|
|
b64d78d58f | ||
|
|
ea81bffdeb | ||
|
|
1e72de6b72 | ||
|
|
92fc243755 | ||
|
|
3471fbf8dc | ||
|
|
b797f773c7 | ||
|
|
dad78f31f3 | ||
|
|
be027a9899 | ||
|
|
87b4bbb94f | ||
|
|
4c2f67a1d0 | ||
|
|
e69682678f | ||
|
|
a2be1aabfa | ||
|
|
ce99474317 | ||
|
|
f4f8ed98d9 | ||
|
|
6eca47b16c | ||
|
|
48f6c1eba4 | ||
|
|
b0cb39cda1 | ||
|
|
c09578d060 | ||
|
|
a75360ccd6 | ||
|
|
5b68dcc8c1 | ||
|
|
3862a1e1d5 | ||
|
|
be107f92d3 | ||
|
|
9245d813c6 | ||
|
|
f7a7957a11 | ||
|
|
49e2d3a7bd | ||
|
|
b46c5ae82a | ||
|
|
7e6c5a2db4 | ||
|
|
9112e78925 | ||
|
|
3b18e65c6a | ||
|
|
6ac6ddbb47 | ||
|
|
9687f3700d | ||
|
|
2263d9c44b | ||
|
|
387b68fe11 | ||
|
|
df2561f6a2 | ||
|
|
96a555fc5a | ||
|
|
0f4359116e | ||
|
|
9ff51ca17f | ||
|
|
045f995203 | ||
|
|
f6cd24499b | ||
|
|
51eb0b2cb7 | ||
|
|
d379a25ae4 | ||
|
|
69f9c17555 | ||
|
|
1a30b2d73f | ||
|
|
57a44846ae | ||
|
|
a9c17dbf93 | ||
|
|
2d3ae485e3 | ||
|
|
b9ebf7cf14 | ||
|
|
12100320d2 | ||
|
|
73fa7dd7af | ||
|
|
88c7d19d54 | ||
|
|
e2d652ec4d | ||
|
|
3f8e8b04fd | ||
|
|
3e71e0ef68 | ||
|
|
7b73c9628d | ||
|
|
d92ef4c215 | ||
|
|
27575cd52d | ||
|
|
ef6f66bb9a | ||
|
|
1410682fb6 | ||
|
|
283a84724f | ||
|
|
e1530cdfcc | ||
|
|
5eb8a2a86a | ||
|
|
d8286d0dc2 | ||
|
|
51288221ce | ||
|
|
06302e30ae | ||
|
|
311352d195 | ||
|
|
0df11253ec | ||
|
|
f18beaa1e4 | ||
|
|
7985f5243a | ||
|
|
ff168a806e | ||
|
|
bb7033174c | ||
|
|
7e4788e383 | ||
|
|
9cb332f0e2 | ||
|
|
0c1510739c | ||
|
|
06134e9521 | ||
|
|
0d19f5d421 | ||
|
|
d41f6a8752 | ||
|
|
768df4ff7a | ||
|
|
e3211ff88b | ||
|
|
49c206fe1e |
15
.github/workflows/docker-file-build.yml
vendored
Normal file
15
.github/workflows/docker-file-build.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: "Dockerfile build"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Build Docker image"
|
||||
run: docker build .
|
||||
12
.github/workflows/go-licenses.yml
vendored
12
.github/workflows/go-licenses.yml
vendored
@@ -17,7 +17,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tailscale:
|
||||
update-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
@@ -50,11 +50,11 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply@tailscale.com>
|
||||
committer: License Updater <noreply@tailscale.com>
|
||||
author: License Updater <noreply+license-updater@tailscale.com>
|
||||
committer: License Updater <noreply+license-updater@tailscale.com>
|
||||
branch: licenses/cli
|
||||
commit-message: "licenses: update tailscale{,d} licenses"
|
||||
title: "licenses: update tailscale{,d} licenses"
|
||||
|
||||
40
.github/workflows/golangci-lint.yml
vendored
Normal file
40
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
# For now, only lint pull requests, not the main branches.
|
||||
pull_request:
|
||||
|
||||
# TODO(andrew): enable for main branch after an initial waiting period.
|
||||
#push:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: golangci-lint
|
||||
# Note: this is the 'v3' tag as of 2023-04-17
|
||||
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
|
||||
with:
|
||||
version: v1.52.2
|
||||
|
||||
# Show only new issues if it's a pull request.
|
||||
only-new-issues: true
|
||||
37
.github/workflows/govulncheck.yml
vendored
Normal file
37
.github/workflows/govulncheck.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: govulncheck
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # 8am EST / 10am PST / 12pm UTC
|
||||
workflow_dispatch: # allow manual trigger for testing
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/govulncheck.yml"
|
||||
|
||||
jobs:
|
||||
source-scan:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install govulncheck
|
||||
run: ./tool/go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
- name: Scan source code for known vulnerabilities
|
||||
run: PATH=$PWD/tool/:$PATH "$(./tool/go env GOPATH)/bin/govulncheck" -test ./...
|
||||
|
||||
- uses: ruby/action-slack@v3.2.1
|
||||
with:
|
||||
payload: >
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks>
|
||||
(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|commit>) of ${{ github.repository }}@${{ github.ref_name }} by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'schedule'
|
||||
102
.github/workflows/installer.yml
vendored
Normal file
102
.github/workflows/installer.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: test installer.sh
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- scripts/installer.sh
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
paths:
|
||||
- scripts/installer.sh
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
# Don't abort the entire matrix if one element fails.
|
||||
fail-fast: false
|
||||
# Don't start all of these at once, which could saturate Github workers.
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
image:
|
||||
# This is a list of Docker images against which we test our installer.
|
||||
# If you find that some of these no longer exist, please feel free
|
||||
# to remove them from the list.
|
||||
# When adding new images, please only use official ones.
|
||||
- "debian:oldstable-slim"
|
||||
- "debian:stable-slim"
|
||||
- "debian:testing-slim"
|
||||
- "debian:sid-slim"
|
||||
- "ubuntu:18.04"
|
||||
- "ubuntu:20.04"
|
||||
- "ubuntu:22.04"
|
||||
- "ubuntu:22.10"
|
||||
- "ubuntu:23.04"
|
||||
- "elementary/docker:stable"
|
||||
- "elementary/docker:unstable"
|
||||
- "parrotsec/core:lts-amd64"
|
||||
- "parrotsec/core:latest"
|
||||
- "kalilinux/kali-rolling"
|
||||
- "kalilinux/kali-dev"
|
||||
- "oraclelinux:9"
|
||||
- "oraclelinux:8"
|
||||
- "fedora:latest"
|
||||
- "rockylinux:8.7"
|
||||
- "rockylinux:9"
|
||||
- "amazonlinux:latest"
|
||||
- "opensuse/leap:latest"
|
||||
- "opensuse/tumbleweed:latest"
|
||||
- "archlinux:latest"
|
||||
- "alpine:3.14"
|
||||
- "alpine:latest"
|
||||
- "alpine:edge"
|
||||
deps:
|
||||
# Run all images installing curl as a dependency.
|
||||
- curl
|
||||
include:
|
||||
# Check a few images with wget rather than curl.
|
||||
- { image: "debian:oldstable-slim", deps: "wget" }
|
||||
- { image: "debian:sid-slim", deps: "wget" }
|
||||
- { image: "ubuntu:23.04", deps: "wget" }
|
||||
# Ubuntu 16.04 also needs apt-transport-https installed.
|
||||
- { image: "ubuntu:16.04", deps: "curl apt-transport-https" }
|
||||
- { image: "ubuntu:16.04", deps: "wget apt-transport-https" }
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
steps:
|
||||
- name: install dependencies (yum)
|
||||
# tar and gzip are needed by the actions/checkout below.
|
||||
run: yum install -y --allowerasing tar gzip ${{ matrix.deps }}
|
||||
if: |
|
||||
contains(matrix.image, 'centos')
|
||||
|| contains(matrix.image, 'oraclelinux')
|
||||
|| contains(matrix.image, 'fedora')
|
||||
|| contains(matrix.image, 'amazonlinux')
|
||||
- name: install dependencies (zypper)
|
||||
# tar and gzip are needed by the actions/checkout below.
|
||||
run: zypper --non-interactive install tar gzip
|
||||
if: contains(matrix.image, 'opensuse')
|
||||
- name: install dependencies (apt-get)
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y ${{ matrix.deps }}
|
||||
if: |
|
||||
contains(matrix.image, 'debian')
|
||||
|| contains(matrix.image, 'ubuntu')
|
||||
|| contains(matrix.image, 'elementary')
|
||||
|| contains(matrix.image, 'parrotsec')
|
||||
|| contains(matrix.image, 'kalilinux')
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: run installer
|
||||
run: scripts/installer.sh
|
||||
# Package installation can fail in docker because systemd is not running
|
||||
# as PID 1, so ignore errors at this step. The real check is the
|
||||
# `tailscale --version` command below.
|
||||
continue-on-error: true
|
||||
- name: check tailscale version
|
||||
run: tailscale --version
|
||||
92
.github/workflows/test.yml
vendored
92
.github/workflows/test.yml
vendored
@@ -46,14 +46,31 @@ jobs:
|
||||
include:
|
||||
- goarch: amd64
|
||||
- goarch: amd64
|
||||
variant: race
|
||||
buildflags: "-race"
|
||||
- goarch: "386" # thanks yaml
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# Note: unlike the other setups, this is only grabbing the mod download
|
||||
# cache, rather than the whole mod directory, as the download cache
|
||||
# contains zips that can be unpacked in parallel faster than they can be
|
||||
# fetched and extracted by tar
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod/cache
|
||||
~\AppData\Local\go-build
|
||||
# The -2- here should be incremented when the scheme of data to be
|
||||
# cached changes (e.g. path above changes).
|
||||
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
|
||||
- name: build all
|
||||
run: ./tool/go build ./...
|
||||
run: ./tool/go build ${{matrix.buildflags}} ./...
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: build variant CLIs
|
||||
@@ -73,13 +90,11 @@ jobs:
|
||||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: test all
|
||||
if: matrix.variant != 'race'
|
||||
run: ./tool/go test -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: test all (race)
|
||||
if: matrix.variant == 'race'
|
||||
run: ./tool/go test -race -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
|
||||
- name: bench all
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: check that no tracked files changed
|
||||
@@ -101,6 +116,13 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -109,17 +131,20 @@ jobs:
|
||||
# contains zips that can be unpacked in parallel faster than they can be
|
||||
# fetched and extracted by tar
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod/cache
|
||||
~\AppData\Local\go-build
|
||||
# The -2- here should be incremented when the scheme of data to be
|
||||
# cached changes (e.g. path above changes).
|
||||
# TODO(raggi): add a go version here.
|
||||
key: ${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-go-2-
|
||||
- name: test
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: ./tool/go test -bench . -benchtime 1x ./...
|
||||
run: go test -bench . -benchtime 1x ./...
|
||||
|
||||
vm:
|
||||
runs-on: ["self-hosted", "linux", "vm"]
|
||||
@@ -174,6 +199,23 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# Note: unlike the other setups, this is only grabbing the mod download
|
||||
# cache, rather than the whole mod directory, as the download cache
|
||||
# contains zips that can be unpacked in parallel faster than they can be
|
||||
# fetched and extracted by tar
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod/cache
|
||||
~\AppData\Local\go-build
|
||||
# The -2- here should be incremented when the scheme of data to be
|
||||
# cached changes (e.g. path above changes).
|
||||
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-
|
||||
- name: build all
|
||||
run: ./tool/go build ./cmd/...
|
||||
env:
|
||||
@@ -223,6 +265,23 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# Note: unlike the other setups, this is only grabbing the mod download
|
||||
# cache, rather than the whole mod directory, as the download cache
|
||||
# contains zips that can be unpacked in parallel faster than they can be
|
||||
# fetched and extracted by tar
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod/cache
|
||||
~\AppData\Local\go-build
|
||||
# The -2- here should be incremented when the scheme of data to be
|
||||
# cached changes (e.g. path above changes).
|
||||
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-go-2-
|
||||
- name: build tsconnect client
|
||||
run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
|
||||
env:
|
||||
@@ -235,6 +294,15 @@ jobs:
|
||||
./tool/go run ./cmd/tsconnect --fast-compression build
|
||||
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
|
||||
|
||||
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: test tailscale_go
|
||||
run: ./tool/go test -tags=tailscale_go,ts_enable_sockstats ./net/sockstats/...
|
||||
|
||||
|
||||
fuzz:
|
||||
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
|
||||
# of the file), so it's more complex than usual: the 'build fuzzers' step
|
||||
@@ -372,6 +440,7 @@ jobs:
|
||||
- cross
|
||||
- ios
|
||||
- wasm
|
||||
- tailscale_go
|
||||
- fuzz
|
||||
- depaware
|
||||
- go_generate
|
||||
@@ -389,7 +458,7 @@ jobs:
|
||||
# By having the job always run, but skipping its only step as needed, we
|
||||
# let the CI output collapse nicely in PRs.
|
||||
if: failure() && github.event_name == 'push'
|
||||
uses: ruby/action-slack@v3.0.0
|
||||
uses: ruby/action-slack@v3.2.1
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
@@ -416,6 +485,7 @@ jobs:
|
||||
- cross
|
||||
- ios
|
||||
- wasm
|
||||
- tailscale_go
|
||||
- fuzz
|
||||
- depaware
|
||||
- go_generate
|
||||
|
||||
31
.github/workflows/tsconnect-pkg-publish.yml
vendored
31
.github/workflows/tsconnect-pkg-publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: "@tailscale/connect npm publish"
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Build package
|
||||
# Build with build_dist.sh to ensure that version information is embedded.
|
||||
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
|
||||
# also picks up our custom Go toolchain.
|
||||
run: |
|
||||
export TS_USE_TOOLCHAIN=1
|
||||
./build_dist.sh tailscale.com/cmd/tsconnect
|
||||
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
|
||||
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public
|
||||
10
.github/workflows/update-flake.yml
vendored
10
.github/workflows/update-flake.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tailscale:
|
||||
update-flake:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: ./update-flake.sh
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
@@ -35,11 +35,11 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: Flakes Updater <noreply@tailscale.com>
|
||||
committer: Flakes Updater <noreply@tailscale.com>
|
||||
author: Flakes Updater <noreply+flakes-updater@tailscale.com>
|
||||
committer: Flakes Updater <noreply+flakes-updater@tailscale.com>
|
||||
branch: flakes
|
||||
commit-message: "go.mod.sri: update SRI hash for go.mod changes"
|
||||
title: "go.mod.sri: update SRI hash for go.mod changes"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,5 +35,8 @@ cmd/tailscaled/tailscaled
|
||||
# Ignore direnv nix-shell environment cache
|
||||
.direnv/
|
||||
|
||||
.vite/
|
||||
webui/node_modules
|
||||
|
||||
/gocross
|
||||
/dist
|
||||
|
||||
61
.golangci.yml
Normal file
61
.golangci.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
linters:
|
||||
# Don't enable any linters by default; just the ones that we explicitly
|
||||
# enable in the list below.
|
||||
disable-all: true
|
||||
enable:
|
||||
- bidichk
|
||||
- gofmt
|
||||
- goimports
|
||||
- misspell
|
||||
- revive
|
||||
|
||||
# Configuration for how we run golangci-lint
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
issues:
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
# These are forks of an upstream package and thus are exempt from stylistic
|
||||
# changes that would make pulling in upstream changes harder.
|
||||
- path: tempfork/.*\.go
|
||||
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
|
||||
- path: util/singleflight/.*\.go
|
||||
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
|
||||
|
||||
# Per-linter settings are contained in this top-level key
|
||||
linters-settings:
|
||||
# Enable all rules by default; we don't use invisible unicode runes.
|
||||
bidichk:
|
||||
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
|
||||
goimports:
|
||||
|
||||
misspell:
|
||||
|
||||
revive:
|
||||
enable-all-rules: false
|
||||
ignore-generated-header: true
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: context-keys-type
|
||||
- name: defer
|
||||
arguments: [[
|
||||
# Calling 'recover' at the time a defer is registered (i.e. "defer recover()") has no effect.
|
||||
"immediate-recover",
|
||||
# Calling 'recover' outside of a deferred function has no effect
|
||||
"recover",
|
||||
# Returning values from a deferred function has no effect
|
||||
"return",
|
||||
]]
|
||||
- name: duplicated-imports
|
||||
- name: errorf
|
||||
- name: string-of-int
|
||||
- name: time-equal
|
||||
- name: unconditional-recursion
|
||||
- name: useless-break
|
||||
- name: waitgroup-by-value
|
||||
@@ -47,8 +47,7 @@ RUN go install \
|
||||
golang.org/x/crypto/ssh \
|
||||
golang.org/x/crypto/acme \
|
||||
nhooyr.io/websocket \
|
||||
github.com/mdlayher/netlink \
|
||||
golang.zx2c4.com/wireguard/device
|
||||
github.com/mdlayher/netlink
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -73,4 +72,4 @@ RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
# For compat with the previous run.sh, although ideally you should be
|
||||
# using build_docker.sh which sets an entrypoint for the image.
|
||||
RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh
|
||||
RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
|
||||
|
||||
5
Makefile
5
Makefile
@@ -48,11 +48,10 @@ staticcheck: ## Run staticcheck.io checks
|
||||
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
|
||||
|
||||
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
|
||||
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
|
||||
./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH}
|
||||
|
||||
spkall: ## Build synology packages for all architectures and DSM versions
|
||||
mkdir -p spks
|
||||
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
|
||||
./tool/go run ./cmd/dist build synology
|
||||
|
||||
pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
|
||||
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.37.0
|
||||
1.47.0
|
||||
|
||||
@@ -8,14 +8,20 @@
|
||||
package atomicfile // import "tailscale.com/atomicfile"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename. The perm argument is ignored on Windows.
|
||||
// WriteFile writes data to filename+some suffix, then renames it into filename.
|
||||
// The perm argument is ignored on Windows. If the target filename already
|
||||
// exists but is not a regular file, WriteFile returns an error.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err == nil && !fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s already exists and is not a regular file", filename)
|
||||
}
|
||||
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
47
atomicfile/atomicfile_test.go
Normal file
47
atomicfile/atomicfile_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !windows
|
||||
|
||||
package atomicfile
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDoesNotOverwriteIrregularFiles(t *testing.T) {
|
||||
// Per tailscale/tailscale#7658 as one example, almost any imagined use of
|
||||
// atomicfile.Write should likely not attempt to overwrite an irregular file
|
||||
// such as a device node, socket, or named pipe.
|
||||
|
||||
const filename = "TestDoesNotOverwriteIrregularFiles"
|
||||
var path string
|
||||
// macOS private temp does not allow unix socket creation, but /tmp does.
|
||||
if runtime.GOOS == "darwin" {
|
||||
path = filepath.Join("/tmp", filename)
|
||||
t.Cleanup(func() { os.Remove(path) })
|
||||
} else {
|
||||
path = filepath.Join(t.TempDir(), filename)
|
||||
}
|
||||
|
||||
// The least troublesome thing to make that is not a file is a unix socket.
|
||||
// Making a null device sadly requires root.
|
||||
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
err = WriteFile(path, []byte("hello"), 0644)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not a regular file") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
|
||||
go="./tool/go"
|
||||
fi
|
||||
|
||||
eval `$go run ./cmd/mkversion`
|
||||
eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
@@ -49,4 +49,4 @@ while [ "$#" -gt 1 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
exec ./tool/go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"
|
||||
exec $go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
|
||||
// it as a string.
|
||||
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
|
||||
// changes are allowing comments and trailing comments. See the following links for more info:
|
||||
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
|
||||
// https://tailscale.com/s/acl-format
|
||||
// https://github.com/tailscale/hujson
|
||||
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
|
||||
// Format return errors to be descriptive.
|
||||
@@ -150,8 +150,9 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
|
||||
// ACLTestFailureSummary specifies the JSON format sent to the
|
||||
// JavaScript client to be rendered in the HTML.
|
||||
type ACLTestFailureSummary struct {
|
||||
User string `json:"user"`
|
||||
Errors []string `json:"errors"`
|
||||
User string `json:"user,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
// ACLTestError is ErrResponse but with an extra field to account for ACLTestFailureSummary.
|
||||
@@ -436,7 +437,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
|
||||
tests := []ACLTest{{User: source, Allow: []string{dest}}}
|
||||
postData, err := json.Marshal(tests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,12 +10,14 @@ import "tailscale.com/tailcfg"
|
||||
const LocalAPIHost = "local-tailscaled.sock"
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
// In successful whois responses, Node and UserProfile are never nil.
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
UserProfile *tailcfg.UserProfile
|
||||
|
||||
// Caps are extra capabilities that the remote Node has to this node.
|
||||
Caps []string `json:",omitempty"`
|
||||
// CapMap is a map of capabilities to their values.
|
||||
// See tailcfg.PeerCapMap and tailcfg.PeerCapability for details.
|
||||
CapMap tailcfg.PeerCapMap
|
||||
}
|
||||
|
||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
@@ -213,8 +212,20 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error)
|
||||
|
||||
// AuthorizeDevice marks a device as authorized.
|
||||
func (c *Client) AuthorizeDevice(ctx context.Context, deviceID string) error {
|
||||
return c.SetAuthorized(ctx, deviceID, true)
|
||||
}
|
||||
|
||||
// SetAuthorized marks a device as authorized or not.
|
||||
func (c *Client) SetAuthorized(ctx context.Context, deviceID string, authorized bool) error {
|
||||
params := &struct {
|
||||
Authorized bool `json:"authorized"`
|
||||
}{Authorized: authorized}
|
||||
data, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/api/v2/device/%s/authorized", c.baseURL(), url.PathEscape(deviceID))
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(`{"authorized":true}`))
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData interface{}) ([]byte, error) {
|
||||
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
|
||||
data, err := json.Marshal(&postData)
|
||||
if err != nil {
|
||||
|
||||
@@ -68,12 +68,32 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
|
||||
// CreateKey creates a new key for the current user. Currently, only auth keys
|
||||
// can be created. Returns the key itself, which cannot be retrieved again
|
||||
// can be created. It returns the secret key itself, which cannot be retrieved again
|
||||
// later, and the key metadata.
|
||||
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (string, *Key, error) {
|
||||
//
|
||||
// To create a key with a specific expiry, use CreateKeyWithExpiry.
|
||||
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (keySecret string, keyMeta *Key, _ error) {
|
||||
return c.CreateKeyWithExpiry(ctx, caps, 0)
|
||||
}
|
||||
|
||||
// CreateKeyWithExpiry is like CreateKey, but allows specifying a expiration time.
|
||||
//
|
||||
// The time is truncated to a whole number of seconds. If zero, that means no expiration.
|
||||
func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities, expiry time.Duration) (keySecret string, keyMeta *Key, _ error) {
|
||||
|
||||
// convert expirySeconds to an int64 (seconds)
|
||||
expirySeconds := int64(expiry.Seconds())
|
||||
if expirySeconds < 0 {
|
||||
return "", nil, fmt.Errorf("expiry must be positive")
|
||||
}
|
||||
if expirySeconds == 0 && expiry != 0 {
|
||||
return "", nil, fmt.Errorf("non-zero expiry must be at least one second")
|
||||
}
|
||||
|
||||
keyRequest := struct {
|
||||
Capabilities KeyCapabilities `json:"capabilities"`
|
||||
}{caps}
|
||||
Capabilities KeyCapabilities `json:"capabilities"`
|
||||
ExpirySeconds int64 `json:"expirySeconds,omitempty"`
|
||||
}{caps, int64(expirySeconds)}
|
||||
bs, err := json.Marshal(keyRequest)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -95,8 +96,9 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
// We use 127.0.0.1 and not "localhost" (issue 7851).
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
s := safesocket.DefaultConnectionStrategy(lc.socket())
|
||||
@@ -367,6 +369,34 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DebugPortmap invokes the debug-portmap endpoint, and returns an
|
||||
// io.ReadCloser that can be used to read the logs that are printed during this
|
||||
// process.
|
||||
func (lc *LocalClient) DebugPortmap(ctx context.Context, duration time.Duration, ty, gwSelf string) (io.ReadCloser, error) {
|
||||
vals := make(url.Values)
|
||||
vals.Set("duration", duration.String())
|
||||
vals.Set("type", ty)
|
||||
if gwSelf != "" {
|
||||
vals.Set("gateway_and_self", gwSelf)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
|
||||
// The schema (including when keys are re-read) is not a stable interface.
|
||||
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
|
||||
@@ -821,6 +851,30 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
encodedPrivate, err := tkaKey.MarshalText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
@@ -858,6 +912,15 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
@@ -883,6 +946,57 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
||||
// in url and returns information extracted from it.
|
||||
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
||||
vr := struct {
|
||||
URL string
|
||||
}{url}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
||||
}
|
||||
|
||||
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
||||
}
|
||||
|
||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||
func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
||||
vr := struct {
|
||||
Keys []tkatype.KeyID
|
||||
ForkFrom string
|
||||
}{removeKeys, forkFrom.String()}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||
func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
@@ -1010,6 +1124,27 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
|
||||
return err
|
||||
}
|
||||
|
||||
// QueryFeature makes a request for instructions on how to enable a
|
||||
// feature, such as Funnel, for the node's tailnet.
|
||||
//
|
||||
// This request itself does not directly enable the feature on behalf
|
||||
// of the node, but rather returns information that can be presented
|
||||
// to the acting user about where/how to enable the feature.
|
||||
//
|
||||
// If relevant, this includes a control URL the user can visit to
|
||||
// explicitly consent to using the feature. LocalClient.WatchIPNBus
|
||||
// can be used to block on the feature being enabled.
|
||||
//
|
||||
// 2023-08-02: Valid feature values are "serve" and "funnel".
|
||||
func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
|
||||
v := url.Values{"feature": {feature}}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/query-feature?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
|
||||
}
|
||||
|
||||
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
|
||||
v := url.Values{"region": {regionIDOrCode}}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
|
||||
@@ -1039,7 +1174,6 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
res.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
|
||||
@@ -131,6 +131,8 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
} else if ft.Elem().String() == "encoding/json.RawMessage" {
|
||||
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
|
||||
@@ -6,138 +6,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/kube"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// checkSecretPermissions checks the secret access permissions of the current
|
||||
// pod. It returns an error if the basic permissions tailscale needs are
|
||||
// missing, and reports whether the patch permission is additionally present.
|
||||
//
|
||||
// Errors encountered during the access checking process are logged, but ignored
|
||||
// so that the pod tries to fail alive if the permissions exist and there's just
|
||||
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
|
||||
// should always be able to use SSARs to assess their own permissions, but since
|
||||
// we didn't use to check permissions this way we'll be cautious in case some
|
||||
// old version of k8s deviates from the current behavior.
|
||||
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
|
||||
var errs []error
|
||||
for _, verb := range []string{"get", "update"} {
|
||||
ok, err := checkPermission(ctx, verb, secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
|
||||
} else if !ok {
|
||||
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
ok, err := checkPermission(ctx, "patch", secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
|
||||
return false, nil
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// checkPermission reports whether the current pod has permission to use the
|
||||
// given verb (e.g. get, update, patch) on secretName.
|
||||
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
|
||||
sar := map[string]any{
|
||||
"apiVersion": "authorization.k8s.io/v1",
|
||||
"kind": "SelfSubjectAccessReview",
|
||||
"spec": map[string]any{
|
||||
"resourceAttributes": map[string]any{
|
||||
"namespace": kubeNamespace,
|
||||
"verb": verb,
|
||||
"resource": "secrets",
|
||||
"name": secretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
bs, err := json.Marshal(sar)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var res struct {
|
||||
Status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
} `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(bs, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Status.Allowed, nil
|
||||
}
|
||||
|
||||
// findKeyInKubeSecret inspects the kube secret secretName for a data
|
||||
// field called "authkey", and returns its value if present.
|
||||
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
||||
s, err := kc.GetSecret(ctx, secretName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
// Kube secret doesn't exist yet, can't have an authkey.
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
ak, ok := s.Data["authkey"]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// We use a map[string]any here rather than import corev1.Secret,
|
||||
// because we only do very limited things to the secret, and
|
||||
// importing corev1 adds 12MiB to the compiled binary.
|
||||
var s map[string]any
|
||||
if err := json.Unmarshal(bs, &s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if d, ok := s["data"].(map[string]any); ok {
|
||||
if v, ok := d["authkey"].(string); ok {
|
||||
bs, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bs), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
return string(ak), nil
|
||||
}
|
||||
|
||||
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
|
||||
@@ -145,65 +35,38 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
|
||||
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
|
||||
// First check if the secret exists at all. Even if running on
|
||||
// kubernetes, we do not necessarily store state in a k8s secret.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
|
||||
// Assume the secret doesn't exist, or we don't have
|
||||
// permission to access it.
|
||||
return nil
|
||||
if _, err := kc.GetSecret(ctx, secretName); err != nil {
|
||||
if s, ok := err.(*kube.Status); ok {
|
||||
if s.Code >= 400 && s.Code <= 499 {
|
||||
// Assume the secret doesn't exist, or we don't have
|
||||
// permission to access it.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
m := map[string]map[string]string{
|
||||
"stringData": {
|
||||
"device_id": string(deviceID),
|
||||
"device_fqdn": fqdn,
|
||||
m := &kube.Secret{
|
||||
Data: map[string][]byte{
|
||||
"device_id": []byte(deviceID),
|
||||
"device_fqdn": []byte(fqdn),
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
|
||||
if _, err := doKubeRequest(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container")
|
||||
}
|
||||
|
||||
// deleteAuthKey deletes the 'authkey' field of the given kube
|
||||
// secret. No-op if there is no authkey in the secret.
|
||||
func deleteAuthKey(ctx context.Context, secretName string) error {
|
||||
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
|
||||
m := []struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
}{
|
||||
m := []kube.JSONPatch{
|
||||
{
|
||||
Op: "remove",
|
||||
Path: "/data/authkey",
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json-patch+json")
|
||||
if resp, err := doKubeRequest(ctx, req); err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
|
||||
if err := kc.JSONPatchSecret(ctx, secretName, m); err != nil {
|
||||
if s, ok := err.(*kube.Status); ok && s.Code == http.StatusUnprocessableEntity {
|
||||
// This is kubernetes-ese for "the field you asked to
|
||||
// delete already doesn't exist", aka no-op.
|
||||
return nil
|
||||
@@ -213,65 +76,22 @@ func deleteAuthKey(ctx context.Context, secretName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
kubeHost string
|
||||
kubeNamespace string
|
||||
kubeToken string
|
||||
kubeHTTP *http.Transport
|
||||
)
|
||||
var kc *kube.Client
|
||||
|
||||
func initKube(root string) {
|
||||
// If running in Kubernetes, set things up so that doKubeRequest
|
||||
// can talk successfully to the kube apiserver.
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
|
||||
return
|
||||
if root != "/" {
|
||||
// If we are running in a test, we need to set the root path to the fake
|
||||
// service account directory.
|
||||
kube.SetRootPathForTesting(root)
|
||||
}
|
||||
|
||||
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
|
||||
|
||||
bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
|
||||
var err error
|
||||
kc, err = kube.New()
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube namespace: %v", err)
|
||||
log.Fatalf("Error creating kube client: %v", err)
|
||||
}
|
||||
kubeNamespace = strings.TrimSpace(string(bs))
|
||||
|
||||
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube token: %v", err)
|
||||
}
|
||||
kubeToken = strings.TrimSpace(string(bs))
|
||||
|
||||
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube CA cert: %v", err)
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
cp.AppendCertsFromPEM(bs)
|
||||
kubeHTTP = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: cp,
|
||||
},
|
||||
IdleConnTimeout: time.Second,
|
||||
if root != "/" {
|
||||
// If we are running in a test, we need to set the URL to the
|
||||
// httptest server.
|
||||
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
}
|
||||
}
|
||||
|
||||
// doKubeRequest sends r to the kube apiserver.
|
||||
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
|
||||
if kubeHTTP == nil {
|
||||
panic("not in kubernetes")
|
||||
}
|
||||
|
||||
r.URL.Scheme = "https"
|
||||
r.URL.Host = kubeHost
|
||||
r.Header.Set("Authorization", "Bearer "+kubeToken)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := kubeHTTP.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
if cfg.InKubernetes && cfg.KubeSecret != "" {
|
||||
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
|
||||
canPatch, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
|
||||
}
|
||||
|
||||
@@ -607,7 +607,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
}()
|
||||
|
||||
var wantCmds []string
|
||||
for _, p := range test.Phases {
|
||||
for i, p := range test.Phases {
|
||||
lapi.Notify(p.Notify)
|
||||
wantCmds = append(wantCmds, p.WantCmds...)
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||
@@ -626,7 +626,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("phase %d: %v", i, err)
|
||||
}
|
||||
err = tstest.WaitFor(2*time.Second, func() error {
|
||||
for path, want := range p.WantFiles {
|
||||
@@ -983,13 +983,13 @@ func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case "application/strategic-merge-patch+json":
|
||||
req := struct {
|
||||
Data map[string]string `json:"stringData"`
|
||||
Data map[string][]byte `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
for key, val := range req.Data {
|
||||
k.secret[key] = val
|
||||
k.secret[key] = string(val)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
const refreshTimeout = time.Minute
|
||||
@@ -52,6 +53,13 @@ func refreshBootstrapDNS() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
defer cancel()
|
||||
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
|
||||
// Randomize the order of the IPs for each name to avoid the client biasing
|
||||
// to IPv6
|
||||
for k := range dnsEntries {
|
||||
ips := dnsEntries[k]
|
||||
slicesx.Shuffle(ips)
|
||||
dnsEntries[k] = ips
|
||||
}
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
|
||||
@@ -11,14 +11,12 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
prev := *bootstrapDNS
|
||||
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
|
||||
defer func() {
|
||||
*bootstrapDNS = prev
|
||||
}()
|
||||
tstest.Replace(b, bootstrapDNS, "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com")
|
||||
refreshBootstrapDNS()
|
||||
w := new(bitbucketResponseWriter)
|
||||
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)
|
||||
|
||||
@@ -81,7 +81,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: nil,
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
"http/1.1",
|
||||
},
|
||||
GetCertificate: m.getCertificate,
|
||||
}
|
||||
|
||||
@@ -3,26 +3,80 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
|
||||
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
|
||||
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
|
||||
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt
|
||||
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
|
||||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
|
||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
|
||||
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
|
||||
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
|
||||
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+
|
||||
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl
|
||||
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+
|
||||
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl
|
||||
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+
|
||||
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+
|
||||
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
|
||||
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
|
||||
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
|
||||
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto
|
||||
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
nhooyr.io/websocket from tailscale.com/cmd/derper+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
@@ -44,10 +98,13 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netmon from tailscale.com/net/sockstats+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/cmd/derper
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
@@ -58,9 +115,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tsweb from tailscale.com/cmd/derper
|
||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
|
||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
@@ -78,14 +138,17 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/multierr from tailscale.com/health
|
||||
tailscale.com/util/set from tailscale.com/health
|
||||
tailscale.com/util/multierr from tailscale.com/health+
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
@@ -106,11 +169,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/maps from tailscale.com/types/views
|
||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http+
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
@@ -167,14 +231,17 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
expvar from tailscale.com/cmd/derper+
|
||||
flag from tailscale.com/cmd/derper
|
||||
fmt from compress/flate+
|
||||
go/token from google.golang.org/protobuf/internal/strs
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from google.golang.org/protobuf/internal/detrand
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
@@ -186,7 +253,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
net/http from expvar+
|
||||
net/http/httptrace from net/http+
|
||||
net/http/internal from net/http
|
||||
net/http/pprof from tailscale.com/tsweb
|
||||
net/http/pprof from tailscale.com/tsweb+
|
||||
net/netip from go4.org/netipx+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
@@ -199,6 +266,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
regexp from internal/profile+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/crypto/acme+
|
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/pprof from net/http/pprof
|
||||
runtime/trace from net/http/pprof
|
||||
sort from compress/flate+
|
||||
|
||||
@@ -33,11 +33,12 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
|
||||
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
@@ -181,8 +182,9 @@ func main() {
|
||||
}
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tsweb.AddBrowserHeaders(w)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
io.WriteString(w, `<html><body>
|
||||
@@ -202,6 +204,7 @@ func main() {
|
||||
}
|
||||
}))
|
||||
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tsweb.AddBrowserHeaders(w)
|
||||
io.WriteString(w, "User-agent: *\nDisallow: /\n")
|
||||
}))
|
||||
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent))
|
||||
@@ -276,18 +279,6 @@ func main() {
|
||||
defer tlsActiveVersion.Add(label, -1)
|
||||
}
|
||||
|
||||
// Set HTTP headers to appease automated security scanners.
|
||||
//
|
||||
// Security automation gets cranky when HTTPS sites don't
|
||||
// set HSTS, and when they don't specify a content
|
||||
// security policy for XSS mitigation.
|
||||
//
|
||||
// DERP's HTTP interface is only ever used for debug
|
||||
// access (for which trivial safe policies work just
|
||||
// fine), and by DERP clients which don't obey any of
|
||||
// these browser-centric headers anyway.
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
|
||||
mux.ServeHTTP(w, r)
|
||||
})
|
||||
if *httpPort > -1 {
|
||||
@@ -436,11 +427,7 @@ func defaultMeshPSKFile() string {
|
||||
}
|
||||
|
||||
func rateLimitedListenAndServeTLS(srv *http.Server) error {
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
ln, err := net.Listen("tcp", cmpx.Or(srv.Addr, ":https"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
@@ -23,13 +22,14 @@ var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
||||
spread = flag.Bool("spread", true, "whether to spread probing over time")
|
||||
interval = flag.Duration("interval", 15*time.Second, "probe interval")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
p := prober.New().WithSpread(true).WithOnce(*probeOnce)
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
|
||||
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -52,7 +52,6 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux)
|
||||
expvar.Publish("derpprobe", p.Expvar())
|
||||
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
|
||||
log.Fatal(http.ListenAndServe(*listen, mux))
|
||||
}
|
||||
|
||||
27
cmd/dist/dist.go
vendored
27
cmd/dist/dist.go
vendored
@@ -13,15 +13,38 @@ import (
|
||||
|
||||
"tailscale.com/release/dist"
|
||||
"tailscale.com/release/dist/cli"
|
||||
"tailscale.com/release/dist/synology"
|
||||
"tailscale.com/release/dist/unixpkgs"
|
||||
)
|
||||
|
||||
func getTargets() ([]dist.Target, error) {
|
||||
return unixpkgs.Targets(), nil
|
||||
var synologyPackageCenter bool
|
||||
|
||||
func getTargets(signers unixpkgs.Signers) ([]dist.Target, error) {
|
||||
var ret []dist.Target
|
||||
|
||||
ret = append(ret, unixpkgs.Targets(signers)...)
|
||||
// Synology packages can be built either for sideloading, or for
|
||||
// distribution by Synology in their package center. When
|
||||
// distributed through the package center, apps can request
|
||||
// additional permissions to use a tuntap interface and control
|
||||
// the NAS's network stack, rather than be forced to run in
|
||||
// userspace mode.
|
||||
//
|
||||
// Since only we can provide packages to Synology for
|
||||
// distribution, we default to building the "sideload" variant of
|
||||
// packages that we distribute on pkgs.tailscale.com.
|
||||
ret = append(ret, synology.Targets(synologyPackageCenter)...)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd := cli.CLI(getTargets)
|
||||
for _, subcmd := range cmd.Subcommands {
|
||||
if subcmd.Name == "build" {
|
||||
subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center")
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// get-authkey allocates an authkey using an OAuth API client
|
||||
// https://tailscale.com/kb/1215/oauth-clients/ and prints it
|
||||
// https://tailscale.com/s/oauth-clients and prints it
|
||||
// to stdout for scripts to capture and use.
|
||||
package main
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -29,9 +30,9 @@ func main() {
|
||||
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
|
||||
flag.Parse()
|
||||
|
||||
clientId := os.Getenv("TS_API_CLIENT_ID")
|
||||
clientID := os.Getenv("TS_API_CLIENT_ID")
|
||||
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
if clientID == "" || clientSecret == "" {
|
||||
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
|
||||
}
|
||||
|
||||
@@ -39,22 +40,19 @@ func main() {
|
||||
log.Fatal("at least one tag must be specified")
|
||||
}
|
||||
|
||||
baseUrl := os.Getenv("TS_BASE_URL")
|
||||
if baseUrl == "" {
|
||||
baseUrl = "https://api.tailscale.com"
|
||||
}
|
||||
baseURL := cmpx.Or(os.Getenv("TS_BASE_URL"), "https://api.tailscale.com")
|
||||
|
||||
credentials := clientcredentials.Config{
|
||||
ClientID: clientId,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
TokenURL: baseUrl + "/api/v2/oauth/token",
|
||||
TokenURL: baseURL + "/api/v2/oauth/token",
|
||||
Scopes: []string{"device"},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tsClient := tailscale.NewClient("-", nil)
|
||||
tsClient.HTTPClient = credentials.Client(ctx)
|
||||
tsClient.BaseURL = baseUrl
|
||||
tsClient.BaseURL = baseURL
|
||||
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/tailscale/hujson"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
@@ -42,9 +44,9 @@ func modifiedExternallyError() {
|
||||
}
|
||||
}
|
||||
|
||||
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -73,7 +75,7 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
||||
if err := applyNewACL(ctx, client, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -83,9 +85,9 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
}
|
||||
}
|
||||
|
||||
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,16 +115,16 @@ func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
|
||||
if err := testNewACLs(ctx, client, tailnet, apiKey, *policyFname); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func getChecksums(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,8 +153,24 @@ func main() {
|
||||
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
|
||||
}
|
||||
apiKey, ok := os.LookupEnv("TS_API_KEY")
|
||||
if !ok {
|
||||
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
|
||||
oauthId, oiok := os.LookupEnv("TS_OAUTH_ID")
|
||||
oauthSecret, osok := os.LookupEnv("TS_OAUTH_SECRET")
|
||||
if !ok && (!oiok || !osok) {
|
||||
log.Fatal("set envvar TS_API_KEY to your Tailscale API key or TS_OAUTH_ID and TS_OAUTH_SECRET to your Tailscale OAuth ID and Secret")
|
||||
}
|
||||
if ok && (oiok || osok) {
|
||||
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
|
||||
}
|
||||
var client *http.Client
|
||||
if oiok {
|
||||
oauthConfig := &clientcredentials.Config{
|
||||
ClientID: oauthId,
|
||||
ClientSecret: oauthSecret,
|
||||
TokenURL: fmt.Sprintf("https://%s/api/v2/oauth/token", *apiServer),
|
||||
}
|
||||
client = oauthConfig.Client(context.Background())
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
cache, err := LoadCache(*cacheFname)
|
||||
if err != nil {
|
||||
@@ -169,7 +187,7 @@ func main() {
|
||||
ShortUsage: "gitops-pusher [options] apply",
|
||||
ShortHelp: "Pushes changes to CONTROL",
|
||||
LongHelp: `Pushes changes to CONTROL`,
|
||||
Exec: apply(cache, tailnet, apiKey),
|
||||
Exec: apply(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
testCmd := &ffcli.Command{
|
||||
@@ -177,7 +195,7 @@ func main() {
|
||||
ShortUsage: "gitops-pusher [options] test",
|
||||
ShortHelp: "Tests ACL changes",
|
||||
LongHelp: "Tests ACL changes",
|
||||
Exec: test(cache, tailnet, apiKey),
|
||||
Exec: test(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
cksumCmd := &ffcli.Command{
|
||||
@@ -185,7 +203,7 @@ func main() {
|
||||
ShortUsage: "Shows checksums of ACL files",
|
||||
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
Exec: getChecksums(cache, tailnet, apiKey),
|
||||
Exec: getChecksums(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
root := &ffcli.Command{
|
||||
@@ -228,7 +246,7 @@ func sumFile(fname string) (string, error) {
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
|
||||
func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname, oldEtag string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -244,7 +262,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
req.Header.Set("If-Match", `"`+oldEtag+`"`)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -253,7 +271,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
got := resp.StatusCode
|
||||
want := http.StatusOK
|
||||
if got != want {
|
||||
var ate ACLTestError
|
||||
var ate ACLGitopsTestError
|
||||
err := json.NewDecoder(resp.Body).Decode(&ate)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -265,7 +283,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname string) error {
|
||||
data, err := os.ReadFile(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -283,13 +301,13 @@ func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var ate ACLTestError
|
||||
var ate ACLGitopsTestError
|
||||
err = json.NewDecoder(resp.Body).Decode(&ate)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -310,12 +328,12 @@ func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error
|
||||
|
||||
var lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)
|
||||
|
||||
type ACLTestError struct {
|
||||
Message string `json:"message"`
|
||||
Data []ACLTestErrorDetail `json:"data"`
|
||||
// ACLGitopsTestError is redefined here so we can add a custom .Error() response
|
||||
type ACLGitopsTestError struct {
|
||||
tailscale.ACLTestError
|
||||
}
|
||||
|
||||
func (ate ACLTestError) Error() string {
|
||||
func (ate ACLGitopsTestError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
if *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {
|
||||
@@ -332,21 +350,29 @@ func (ate ACLTestError) Error() string {
|
||||
fmt.Fprintln(&sb)
|
||||
|
||||
for _, data := range ate.Data {
|
||||
fmt.Fprintf(&sb, "For user %s:\n", data.User)
|
||||
for _, err := range data.Errors {
|
||||
fmt.Fprintf(&sb, "- %s\n", err)
|
||||
if data.User != "" {
|
||||
fmt.Fprintf(&sb, "For user %s:\n", data.User)
|
||||
}
|
||||
|
||||
if len(data.Errors) > 0 {
|
||||
fmt.Fprint(&sb, "Errors found:\n")
|
||||
for _, err := range data.Errors {
|
||||
fmt.Fprintf(&sb, "- %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(data.Warnings) > 0 {
|
||||
fmt.Fprint(&sb, "Warnings found:\n")
|
||||
for _, err := range data.Warnings {
|
||||
fmt.Fprintf(&sb, "- %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type ACLTestErrorDetail struct {
|
||||
User string `json:"user"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
|
||||
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -355,7 +381,7 @@ func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Accept", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
55
cmd/gitops-pusher/gitops-pusher_test.go
Normal file
55
cmd/gitops-pusher/gitops-pusher_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
func TestEmbeddedTypeUnmarshal(t *testing.T) {
|
||||
var gitopsErr ACLGitopsTestError
|
||||
gitopsErr.Message = "gitops response error"
|
||||
gitopsErr.Data = []tailscale.ACLTestFailureSummary{
|
||||
{
|
||||
User: "GitopsError",
|
||||
Errors: []string{"this was initially created as a gitops error"},
|
||||
},
|
||||
}
|
||||
|
||||
var aclTestErr tailscale.ACLTestError
|
||||
aclTestErr.Message = "native ACL response error"
|
||||
aclTestErr.Data = []tailscale.ACLTestFailureSummary{
|
||||
{
|
||||
User: "ACLError",
|
||||
Errors: []string{"this was initially created as an ACL error"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
|
||||
b, _ := json.Marshal(aclTestErr)
|
||||
var e ACLGitopsTestError
|
||||
err := json.Unmarshal(b, &e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(e.Error(), "For user ACLError") { // the gitops error prints out the user, the acl error doesn't
|
||||
t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
|
||||
}
|
||||
})
|
||||
t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
|
||||
b, _ := json.Marshal(gitopsErr)
|
||||
var e tailscale.ACLTestError
|
||||
err := json.Unmarshal(b, &e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedErr := `Status: 0, Message: "gitops response error", Data: [{User:GitopsError Errors:[this was initially created as a gitops error] Warnings:[]}]`
|
||||
if e.Error() != expectedErr {
|
||||
t.Fatalf("got %v\n, expected %v", e.Error(), expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
name: tailscale-auth-proxy
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["users"]
|
||||
resources: ["users", "groups"]
|
||||
verbs: ["impersonate"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -7,8 +7,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -23,9 +25,8 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -36,7 +37,6 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
"sigs.k8s.io/yaml"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -62,6 +63,7 @@ func main() {
|
||||
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
|
||||
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
|
||||
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
||||
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
|
||||
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
||||
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
|
||||
)
|
||||
@@ -99,9 +101,9 @@ func main() {
|
||||
tsClient.HTTPClient = credentials.Client(context.Background())
|
||||
|
||||
if shouldRunAuthProxy {
|
||||
hostinfo.SetPackage("k8s-operator-proxy")
|
||||
hostinfo.SetApp("k8s-operator-proxy")
|
||||
} else {
|
||||
hostinfo.SetPackage("k8s-operator")
|
||||
hostinfo.SetApp("k8s-operator")
|
||||
}
|
||||
|
||||
s := &tsnet.Server{
|
||||
@@ -166,7 +168,7 @@ waitOnline:
|
||||
loginDone = true
|
||||
case "NeedsMachineAuth":
|
||||
if !machineAuthShown {
|
||||
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
|
||||
startlog.Infof("Machine approval required, please visit the admin panel to approve")
|
||||
machineAuthShown = true
|
||||
}
|
||||
default:
|
||||
@@ -181,32 +183,33 @@ waitOnline:
|
||||
// the cache that sits a few layers below the builder stuff, which will
|
||||
// implicitly filter what parts of the world the builder code gets to see at
|
||||
// all.
|
||||
nsFilter := cache.ObjectSelector{
|
||||
Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
|
||||
nsFilter := cache.ByObject{
|
||||
Field: client.InNamespace(tsNamespace).AsSelector(),
|
||||
}
|
||||
restConfig := config.GetConfigOrDie()
|
||||
mgr, err := manager.New(restConfig, manager.Options{
|
||||
NewCache: cache.BuilderWithOptions(cache.Options{
|
||||
SelectorsByObject: map[client.Object]cache.ObjectSelector{
|
||||
Cache: cache.Options{
|
||||
ByObject: map[client.Object]cache.ByObject{
|
||||
&corev1.Secret{}: nsFilter,
|
||||
&appsv1.StatefulSet{}: nsFilter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create manager: %v", err)
|
||||
}
|
||||
|
||||
sr := &ServiceReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
Client: mgr.GetClient(),
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
proxyPriorityClassName: priorityClassName,
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
}
|
||||
|
||||
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
|
||||
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(_ context.Context, o client.Object) []reconcile.Request {
|
||||
ls := o.GetLabels()
|
||||
if ls[LabelManaged] != "true" {
|
||||
return nil
|
||||
@@ -226,24 +229,34 @@ waitOnline:
|
||||
err = builder.
|
||||
ControllerManagedBy(mgr).
|
||||
For(&corev1.Service{}).
|
||||
Watches(&source.Kind{Type: &appsv1.StatefulSet{}}, reconcileFilter).
|
||||
Watches(&source.Kind{Type: &corev1.Secret{}}, reconcileFilter).
|
||||
Watches(&appsv1.StatefulSet{}, reconcileFilter).
|
||||
Watches(&corev1.Secret{}, reconcileFilter).
|
||||
Complete(sr)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create controller: %v", err)
|
||||
}
|
||||
|
||||
startlog.Infof("Startup complete, operator running")
|
||||
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
|
||||
if shouldRunAuthProxy {
|
||||
rc, err := rest.TransportFor(restConfig)
|
||||
cfg, err := restConfig.TransportConfig()
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest transport: %v", err)
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
authProxyListener, err := s.Listen("tcp", ":443")
|
||||
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not listen on :443: %v", err)
|
||||
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
|
||||
}
|
||||
go runAuthProxy(lc, authProxyListener, rc, zlog.Named("auth-proxy").Infof)
|
||||
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||
|
||||
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
|
||||
}
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
startlog.Fatalf("could not start manager: %v", err)
|
||||
@@ -266,11 +279,12 @@ const (
|
||||
// ServiceReconciler is a simple ControllerManagedBy example implementation.
|
||||
type ServiceReconciler struct {
|
||||
client.Client
|
||||
tsClient tsClient
|
||||
defaultTags []string
|
||||
operatorNamespace string
|
||||
proxyImage string
|
||||
logger *zap.SugaredLogger
|
||||
tsClient tsClient
|
||||
defaultTags []string
|
||||
operatorNamespace string
|
||||
proxyImage string
|
||||
proxyPriorityClassName string
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type tsClient interface {
|
||||
@@ -554,6 +568,9 @@ func (a *ServiceReconciler) getDeviceInfo(ctx context.Context, svc *corev1.Servi
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if sec == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
id = string(sec.Data["device_id"])
|
||||
if id == "" {
|
||||
return "", "", nil
|
||||
@@ -577,6 +594,7 @@ func (a *ServiceReconciler) newAuthKey(ctx context.Context, tags []string) (stri
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
key, _, err := a.tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -621,6 +639,7 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
|
||||
ss.Spec.Template.ObjectMeta.Labels = map[string]string{
|
||||
"app": string(parentSvc.UID),
|
||||
}
|
||||
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
|
||||
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -65,7 +64,7 @@ func TestLoadBalancerClass(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -111,6 +110,8 @@ func TestLoadBalancerClass(t *testing.T) {
|
||||
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
s.Spec.Type = corev1.ServiceTypeClusterIP
|
||||
s.Spec.LoadBalancerClass = nil
|
||||
})
|
||||
mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
// Fake client doesn't automatically delete the LoadBalancer status when
|
||||
// changing away from the LoadBalancer type, we have to do
|
||||
// controller-manager's work by hand.
|
||||
@@ -186,7 +187,7 @@ func TestAnnotations(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -283,7 +284,7 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, since it would have normally happened at
|
||||
@@ -327,7 +328,7 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
// None of the proxy machinery should have changed...
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
// ... but the service should have a LoadBalancer status.
|
||||
|
||||
want = &corev1.Service{
|
||||
@@ -399,7 +400,7 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -448,6 +449,8 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
}
|
||||
s.Spec.Type = corev1.ServiceTypeClusterIP
|
||||
s.Spec.LoadBalancerClass = nil
|
||||
})
|
||||
mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
// Fake client doesn't automatically delete the LoadBalancer status when
|
||||
// changing away from the LoadBalancer type, we have to do
|
||||
// controller-manager's work by hand.
|
||||
@@ -456,7 +459,7 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -523,7 +526,7 @@ func TestCustomHostname(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla"))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla", ""))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -582,6 +585,51 @@ func TestCustomHostname(t *testing.T) {
|
||||
expectEqual(t, fc, want)
|
||||
}
|
||||
|
||||
func TestCustomPriorityClassName(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
proxyPriorityClassName: "tailscale-critical",
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
"tailscale.com/hostname": "custom-priority-class-name",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "custom-priority-class-name", "tailscale-critical"))
|
||||
}
|
||||
|
||||
func expectedSecret(name string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -630,7 +678,7 @@ func expectedHeadlessService(name string) *corev1.Service {
|
||||
}
|
||||
}
|
||||
|
||||
func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
|
||||
func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
@@ -659,6 +707,7 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: "proxies",
|
||||
PriorityClassName: priorityClassName,
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "sysctler",
|
||||
@@ -670,11 +719,11 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []v1.Container{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []v1.EnvVar{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
|
||||
@@ -732,6 +781,21 @@ func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, n
|
||||
}
|
||||
}
|
||||
|
||||
func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
|
||||
t.Helper()
|
||||
obj := O(new(T))
|
||||
if err := client.Get(context.Background(), types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
}, obj); err != nil {
|
||||
t.Fatalf("getting %q: %v", name, err)
|
||||
}
|
||||
update(obj)
|
||||
if err := client.Status().Update(context.Background(), obj); err != nil {
|
||||
t.Fatalf("updating %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
|
||||
t.Helper()
|
||||
got := O(new(T))
|
||||
@@ -815,7 +879,6 @@ func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabili
|
||||
k := &tailscale.Key{
|
||||
ID: "key",
|
||||
Created: time.Now(),
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
Capabilities: caps,
|
||||
}
|
||||
return "secret-authkey", k, nil
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -41,23 +41,42 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.rp.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripper, logf logger.Logf) {
|
||||
// runAuthProxy runs an HTTP server that authenticates requests using the
|
||||
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
|
||||
// It listens on :443 and uses the Tailscale HTTPS certificate.
|
||||
// s will be started if it is not already running.
|
||||
// rt is used to proxy requests to the Kubernetes API.
|
||||
//
|
||||
// It never returns.
|
||||
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
|
||||
ln, err := s.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatalf("could not listen on :443: %v", err)
|
||||
}
|
||||
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
if err != nil {
|
||||
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get local client: %v", err)
|
||||
}
|
||||
ap := &authProxy{
|
||||
logf: logf,
|
||||
lc: lc,
|
||||
rp: &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
// Replace the request with the user's identity.
|
||||
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Remove all authentication headers.
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Header.Del("Authorization")
|
||||
r.Header.Del("Impersonate-Group")
|
||||
r.Header.Del("Impersonate-User")
|
||||
r.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
@@ -65,6 +84,19 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
if who.Node.IsTagged() {
|
||||
// Use the nodes FQDN as the username, and the nodes tags as the groups.
|
||||
// "Impersonate-Group" requires "Impersonate-User" to be set.
|
||||
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
|
||||
for _, tag := range who.Node.Tags {
|
||||
r.Header.Add("Impersonate-Group", tag)
|
||||
}
|
||||
} else {
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
}
|
||||
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
@@ -72,9 +104,17 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
|
||||
Transport: rt,
|
||||
},
|
||||
}
|
||||
if err := http.Serve(tls.NewListener(ls, &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
}), ap); err != nil {
|
||||
hs := &http.Server{
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
},
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: ap,
|
||||
}
|
||||
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatalf("runAuthProxy: failed to serve %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func main() {
|
||||
|
||||
arch := winres.Arch(os.Args[1])
|
||||
switch arch {
|
||||
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
|
||||
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
|
||||
default:
|
||||
log.Fatalf("unsupported arch: %s", arch)
|
||||
}
|
||||
|
||||
@@ -11,35 +11,40 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/goreleaser/nfpm"
|
||||
_ "github.com/goreleaser/nfpm/deb"
|
||||
_ "github.com/goreleaser/nfpm/rpm"
|
||||
"github.com/goreleaser/nfpm/v2"
|
||||
_ "github.com/goreleaser/nfpm/v2/deb"
|
||||
"github.com/goreleaser/nfpm/v2/files"
|
||||
_ "github.com/goreleaser/nfpm/v2/rpm"
|
||||
)
|
||||
|
||||
// parseFiles parses a comma-separated list of colon-separated pairs
|
||||
// into a map of filePathOnDisk -> filePathInPackage.
|
||||
func parseFiles(s string) (map[string]string, error) {
|
||||
ret := map[string]string{}
|
||||
// into files.Contents format.
|
||||
func parseFiles(s string, typ string) (files.Contents, error) {
|
||||
if len(s) == 0 {
|
||||
return ret, nil
|
||||
return nil, nil
|
||||
}
|
||||
var contents files.Contents
|
||||
for _, f := range strings.Split(s, ",") {
|
||||
fs := strings.Split(f, ":")
|
||||
if len(fs) != 2 {
|
||||
return nil, fmt.Errorf("unparseable file field %q", f)
|
||||
}
|
||||
ret[fs[0]] = fs[1]
|
||||
contents = append(contents, &files.Content{Type: files.TypeFile, Source: fs[0], Destination: fs[1]})
|
||||
}
|
||||
return ret, nil
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func parseEmptyDirs(s string) []string {
|
||||
func parseEmptyDirs(s string) files.Contents {
|
||||
// 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, ",")
|
||||
var contents files.Contents
|
||||
for _, d := range strings.Split(s, ",") {
|
||||
contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -48,7 +53,7 @@ func main() {
|
||||
description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
|
||||
goarch := flag.String("arch", "amd64", "GOARCH this package is for")
|
||||
pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
|
||||
files := flag.String("files", "", "comma-separated list of files in src:dst form")
|
||||
regularFiles := flag.String("files", "", "comma-separated list of files in src:dst form")
|
||||
configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
|
||||
emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
|
||||
version := flag.String("version", "0.0.0", "version of the package")
|
||||
@@ -60,15 +65,20 @@ func main() {
|
||||
recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
|
||||
flag.Parse()
|
||||
|
||||
filesMap, err := parseFiles(*files)
|
||||
filesList, err := parseFiles(*regularFiles, files.TypeFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Parsing --files: %v", err)
|
||||
}
|
||||
configsMap, err := parseFiles(*configFiles)
|
||||
configsList, err := parseFiles(*configFiles, files.TypeConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Parsing --configs: %v", err)
|
||||
}
|
||||
emptyDirList := parseEmptyDirs(*emptyDirs)
|
||||
contents := append(filesList, append(configsList, emptyDirList...)...)
|
||||
contents, err = files.PrepareForPackager(contents, 0, *pkgType, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Building package contents: %v", err)
|
||||
}
|
||||
info := nfpm.WithDefaults(&nfpm.Info{
|
||||
Name: *name,
|
||||
Arch: *goarch,
|
||||
@@ -79,9 +89,7 @@ func main() {
|
||||
Homepage: "https://www.tailscale.com",
|
||||
License: "MIT",
|
||||
Overridables: nfpm.Overridables{
|
||||
EmptyFolders: emptyDirList,
|
||||
Files: filesMap,
|
||||
ConfigFiles: configsMap,
|
||||
Contents: contents,
|
||||
Scripts: nfpm.Scripts{
|
||||
PostInstall: *postinst,
|
||||
PreRemove: *prerm,
|
||||
|
||||
@@ -43,8 +43,9 @@ import (
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
@@ -136,8 +137,8 @@ func processObject(dec *jsonv2.Decoder) {
|
||||
|
||||
type message struct {
|
||||
Logtail struct {
|
||||
ID logtail.PublicID `json:"id"`
|
||||
Logged time.Time `json:"server_time"`
|
||||
ID logid.PublicID `json:"id"`
|
||||
Logged time.Time `json:"server_time"`
|
||||
} `json:"logtail"`
|
||||
Logged time.Time `json:"logged"`
|
||||
netlogtype.Message
|
||||
@@ -151,10 +152,10 @@ func printMessage(msg message) {
|
||||
if len(traffic) == 0 {
|
||||
return
|
||||
}
|
||||
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) bool {
|
||||
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) int {
|
||||
nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes
|
||||
ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes
|
||||
return nx > ny
|
||||
return cmpx.Compare(ny, nx)
|
||||
})
|
||||
var sum netlogtype.Counts
|
||||
for _, cc := range traffic {
|
||||
|
||||
@@ -56,7 +56,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(info.Node.Tags) != 0 {
|
||||
if info.Node.IsTagged() {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
|
||||
return
|
||||
|
||||
@@ -272,7 +272,7 @@ func (p *proxy) serve(sessionID int64, c net.Conn) error {
|
||||
}
|
||||
if buf[0] != 'S' {
|
||||
p.errors.Add("upstream-bad-protocol", 1)
|
||||
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
|
||||
return fmt.Errorf("upstream didn't acknowledge start-ssl, said %q", buf[0])
|
||||
}
|
||||
tlsConf := &tls.Config{
|
||||
ServerName: p.upstreamHost,
|
||||
|
||||
@@ -147,7 +147,7 @@ func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, i
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
||||
}
|
||||
if len(whois.Node.Tags) != 0 {
|
||||
if whois.Node.IsTagged() {
|
||||
return nil, fmt.Errorf("tagged nodes are not users")
|
||||
}
|
||||
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
||||
|
||||
242
cmd/sniproxy/sniproxy.go
Normal file
242
cmd/sniproxy/sniproxy.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The sniproxy is an outbound SNI proxy. It receives TLS connections over
|
||||
// Tailscale on one or more TCP ports and sends them out to the same SNI
|
||||
// hostname & port on the internet. It only does TCP.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/tcpproxy"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
var (
|
||||
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
|
||||
wgPort = flag.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
|
||||
)
|
||||
|
||||
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
|
||||
|
||||
var (
|
||||
numSessions = clientmetric.NewCounter("sniproxy_sessions")
|
||||
numBadAddrPort = clientmetric.NewCounter("sniproxy_bad_addrport")
|
||||
dnsResponses = clientmetric.NewCounter("sniproxy_dns_responses")
|
||||
dnsFailures = clientmetric.NewCounter("sniproxy_dns_failed")
|
||||
httpPromoted = clientmetric.NewCounter("sniproxy_http_promoted")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *ports == "" {
|
||||
log.Fatal("no ports")
|
||||
}
|
||||
|
||||
hostinfo.SetApp("sniproxy")
|
||||
|
||||
var s server
|
||||
s.ts.Port = uint16(*wgPort)
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.lc = lc
|
||||
|
||||
for _, portStr := range strings.Split(*ports, ",") {
|
||||
ln, err := s.ts.Listen("tcp", ":"+portStr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Serving on port %v ...", portStr)
|
||||
go s.serve(ln)
|
||||
}
|
||||
|
||||
ln, err := s.ts.Listen("udp", ":53")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveDNS(ln)
|
||||
|
||||
if *promoteHTTPS {
|
||||
ln, err := s.ts.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Promoting HTTP to HTTPS ...")
|
||||
go s.promoteHTTPS(ln)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
type server struct {
|
||||
ts tsnet.Server
|
||||
lc *tailscale.LocalClient
|
||||
}
|
||||
|
||||
func (s *server) serve(ln net.Listener) {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveConn(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveDNS(ln net.Listener) {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveDNSConn(c.(nettype.ConnPacketConn))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
|
||||
defer c.Close()
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
buf := make([]byte, 1500)
|
||||
n, err := c.Read(buf)
|
||||
if err != nil {
|
||||
log.Printf("c.Read failed: %v\n ", err)
|
||||
dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
var msg dnsmessage.Message
|
||||
err = msg.Unpack(buf[:n])
|
||||
if err != nil {
|
||||
log.Printf("dnsmessage unpack failed: %v\n ", err)
|
||||
dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = s.dnsResponse(&msg)
|
||||
if err != nil {
|
||||
log.Printf("s.dnsResponse failed: %v\n", err)
|
||||
dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.Write(buf)
|
||||
if err != nil {
|
||||
log.Printf("c.Write failed: %v\n", err)
|
||||
dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
dnsResponses.Add(1)
|
||||
}
|
||||
|
||||
func (s *server) serveConn(c net.Conn) {
|
||||
addrPortStr := c.LocalAddr().String()
|
||||
_, port, err := net.SplitHostPort(addrPortStr)
|
||||
if err != nil {
|
||||
log.Printf("bogus addrPort %q", addrPortStr)
|
||||
numBadAddrPort.Add(1)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var dialer net.Dialer
|
||||
dialer.Timeout = 5 * time.Second
|
||||
|
||||
var p tcpproxy.Proxy
|
||||
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
|
||||
return netutil.NewOneConnListener(c, nil), nil
|
||||
}
|
||||
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
|
||||
numSessions.Add(1)
|
||||
return &tcpproxy.DialProxy{
|
||||
Addr: net.JoinHostPort(sniName, port),
|
||||
DialContext: dialer.DialContext,
|
||||
}, true
|
||||
})
|
||||
p.Start()
|
||||
}
|
||||
|
||||
func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
|
||||
resp := dnsmessage.NewBuilder(buf,
|
||||
dnsmessage.Header{
|
||||
ID: req.Header.ID,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
})
|
||||
resp.EnableCompression()
|
||||
|
||||
if len(req.Questions) == 0 {
|
||||
buf, _ = resp.Finish()
|
||||
return
|
||||
}
|
||||
|
||||
q := req.Questions[0]
|
||||
err = resp.StartQuestions()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Question(q)
|
||||
|
||||
ip4, ip6 := s.ts.TailscaleIPs()
|
||||
err = resp.StartAnswers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch q.Type {
|
||||
case dnsmessage.TypeAAAA:
|
||||
err = resp.AAAAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AAAAResource{AAAA: ip6.As16()},
|
||||
)
|
||||
|
||||
case dnsmessage.TypeA:
|
||||
err = resp.AResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AResource{A: ip4.As4()},
|
||||
)
|
||||
case dnsmessage.TypeSOA:
|
||||
err = resp.SOAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
|
||||
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
|
||||
)
|
||||
case dnsmessage.TypeNS:
|
||||
err = resp.NSResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.NSResource{NS: tsMBox},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return resp.Finish()
|
||||
}
|
||||
|
||||
func (s *server) promoteHTTPS(ln net.Listener) {
|
||||
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
httpPromoted.Add(1)
|
||||
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
|
||||
}))
|
||||
log.Fatalf("promoteHTTPS http.Serve: %v", err)
|
||||
}
|
||||
@@ -6,33 +6,15 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/util/winutil/authenticode"
|
||||
)
|
||||
|
||||
func init() {
|
||||
verifyAuthenticode = verifyAuthenticodeWindows
|
||||
verifyAuthenticode = verifyTailscale
|
||||
}
|
||||
|
||||
func verifyAuthenticodeWindows(path string) error {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := &windows.WinTrustData{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
|
||||
UIChoice: windows.WTD_UI_NONE,
|
||||
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
|
||||
UnionChoice: windows.WTD_CHOICE_FILE,
|
||||
StateAction: windows.WTD_STATEACTION_VERIFY,
|
||||
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
|
||||
FilePath: path16,
|
||||
}),
|
||||
}
|
||||
err = windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||
data.StateAction = windows.WTD_STATEACTION_CLOSE
|
||||
windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||
return err
|
||||
const certSubjectTailscale = "Tailscale Inc."
|
||||
|
||||
func verifyTailscale(path string) error {
|
||||
return authenticode.Verify(path, certSubjectTailscale)
|
||||
}
|
||||
|
||||
@@ -113,12 +113,15 @@ change in the future.
|
||||
loginCmd,
|
||||
logoutCmd,
|
||||
switchCmd,
|
||||
configureCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
sshCmd,
|
||||
funnelCmd,
|
||||
serveCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
@@ -126,16 +129,12 @@ change in the future.
|
||||
certCmd,
|
||||
netlockCmd,
|
||||
licensesCmd,
|
||||
exitNodeCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
if c.UsageFunc == nil {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
}
|
||||
if envknob.UseWIPCode() {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands,
|
||||
idTokenCmd,
|
||||
@@ -146,17 +145,19 @@ change in the future.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "serve"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||
case slices.Contains(args, "update"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
|
||||
case slices.Contains(args, "configure"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
}
|
||||
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
if c.UsageFunc == nil {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -621,9 +622,16 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
|
||||
},
|
||||
{
|
||||
name: "error_long_label",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 64) + ".example.com",
|
||||
},
|
||||
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
@@ -712,10 +720,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf tstest.MemLogger
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
}
|
||||
goos := cmpx.Or(tt.goos, "linux")
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
@@ -1071,20 +1076,42 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "force_reauth_over_ssh_no_risk",
|
||||
flags: []string{"--force-reauth"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "force_reauth_over_ssh",
|
||||
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: nil,
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.sshOverTailscale {
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
|
||||
} else if isSSHOverTailscale() {
|
||||
// The test is being executed over a "real" tailscale SSH
|
||||
// session, but sshOverTailscale is unset. Make the test appear
|
||||
// as if it's not over tailscale SSH.
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
|
||||
}
|
||||
if tt.env.goos == "" {
|
||||
tt.env.goos = "linux"
|
||||
|
||||
@@ -26,12 +26,14 @@ func init() {
|
||||
|
||||
var configureKubeconfigCmd = &ffcli.Command{
|
||||
Name: "kubeconfig",
|
||||
ShortHelp: "Configure kubeconfig to use Tailscale",
|
||||
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
|
||||
ShortUsage: "kubeconfig <hostname-or-fqdn>",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
Run this command to configure your kubeconfig to use Tailscale for authentication to a Kubernetes cluster.
|
||||
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
|
||||
|
||||
The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
|
||||
|
||||
See: https://tailscale.com/s/k8s-auth-proxy
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("kubeconfig")
|
||||
|
||||
@@ -35,13 +35,13 @@ var configureHostCmd = &ffcli.Command{
|
||||
var synologyConfigureCmd = &ffcli.Command{
|
||||
Name: "synology",
|
||||
Exec: runConfigureSynology,
|
||||
ShortHelp: "Configure Synology to enable more Tailscale features",
|
||||
ShortHelp: "Configure Synology to enable outbound connections",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure-host' command is intended to run at boot as root
|
||||
to create the /dev/net/tun device and give the tailscaled binary
|
||||
permission to use it.
|
||||
This command is intended to run at boot as root on a Synology device to
|
||||
create the /dev/net/tun device and give the tailscaled binary permission
|
||||
to use it.
|
||||
|
||||
See: https://tailscale.com/kb/1152/synology-outbound/
|
||||
See: https://tailscale.com/s/synology-outbound
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("synology")
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
|
||||
var configureCmd = &ffcli.Command{
|
||||
Name: "configure",
|
||||
ShortHelp: "Configure the host to enable more Tailscale features",
|
||||
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure' command is intended to provide a way to configure different
|
||||
services on the host to enable more Tailscale features.
|
||||
The 'configure' set of commands are intended to provide a way to enable different
|
||||
services on the host to use Tailscale in more ways.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure")
|
||||
|
||||
@@ -201,6 +201,23 @@ var debugCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "portmap",
|
||||
Exec: debugPortmap,
|
||||
ShortHelp: "run portmap debugging debugging",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("portmap")
|
||||
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
|
||||
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
|
||||
fs.StringVar(&debugPortmapArgs.gwSelf, "gw-self", "", `override gateway and self IP (format: "gatewayIP/selfIP")`)
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "peer-endpoint-changes",
|
||||
Exec: runPeerEndpointChanges,
|
||||
ShortHelp: "prints debug information about a peer's endpoint changes",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -789,3 +806,82 @@ func runCapture(ctx context.Context, args []string) error {
|
||||
_, err = io.Copy(f, stream)
|
||||
return err
|
||||
}
|
||||
|
||||
var debugPortmapArgs struct {
|
||||
duration time.Duration
|
||||
gwSelf string
|
||||
ty string
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context, args []string) error {
|
||||
rc, err := localClient.DebugPortmap(ctx,
|
||||
debugPortmapArgs.duration,
|
||||
debugPortmapArgs.ty,
|
||||
debugPortmapArgs.gwSelf,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
_, err = io.Copy(os.Stdout, rc)
|
||||
return err
|
||||
}
|
||||
|
||||
func runPeerEndpointChanges(ctx context.Context, args []string) error {
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
printf("%s\n", description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: peer-status <hostname-or-IP>")
|
||||
}
|
||||
var ip string
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self {
|
||||
printf("%v is local Tailscale IP\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
if ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/debug-peer-endpoint-changes?ip="+ip, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := localClient.DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dst bytes.Buffer
|
||||
if err := json.Indent(&dst, body, "", " "); err != nil {
|
||||
return fmt.Errorf("indenting returned JSON: %w", err)
|
||||
}
|
||||
|
||||
if ss := dst.String(); !strings.HasSuffix(ss, "\n") {
|
||||
dst.WriteByte('\n')
|
||||
}
|
||||
fmt.Printf("%s", dst.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func isSystemdSystem() bool {
|
||||
return false
|
||||
}
|
||||
switch distro.Get() {
|
||||
case distro.QNAP, distro.Gokrazy, distro.Synology:
|
||||
case distro.QNAP, distro.Gokrazy, distro.Synology, distro.Unraid:
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath("systemctl")
|
||||
|
||||
248
cmd/tailscale/cli/exitnode.go
Normal file
248
cmd/tailscale/cli/exitnode.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
var exitNodeCmd = &ffcli.Command{
|
||||
Name: "exit-node",
|
||||
ShortUsage: "exit-node [flags]",
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ShortUsage: "exit-node list [flags]",
|
||||
ShortHelp: "Show exit nodes",
|
||||
Exec: runExitNodeList,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("list")
|
||||
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
|
||||
},
|
||||
}
|
||||
|
||||
var exitNodeArgs struct {
|
||||
filter string
|
||||
}
|
||||
|
||||
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
|
||||
// If the exit node has location and priority data, only the highest
|
||||
// priority node for each city location is shown to the user.
|
||||
// If the country location has more than one city, an 'Any' city
|
||||
// is returned for the country, which lists the highest priority
|
||||
// node in that country.
|
||||
// For countries without location data, each exit node is displayed.
|
||||
func runExitNodeList(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected non-flag arguments to 'tailscale exit-node list'")
|
||||
}
|
||||
getStatus := localClient.Status
|
||||
st, err := getStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, ps := range st.Peer {
|
||||
if !ps.ExitNodeOption {
|
||||
// We only show location based exit nodes.
|
||||
continue
|
||||
}
|
||||
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
|
||||
if len(peers) == 0 {
|
||||
return errors.New("no exit nodes found")
|
||||
}
|
||||
|
||||
filteredPeers := filterFormatAndSortExitNodes(peers, exitNodeArgs.filter)
|
||||
|
||||
if len(filteredPeers.Countries) == 0 && exitNodeArgs.filter != "" {
|
||||
return fmt.Errorf("no exit nodes found for %q", exitNodeArgs.filter)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", "IP", "HOSTNAME", "COUNTRY", "CITY", "STATUS")
|
||||
for _, country := range filteredPeers.Countries {
|
||||
for _, city := range country.Cities {
|
||||
for _, peer := range city.Peers {
|
||||
|
||||
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// peerStatus returns a string representing the current state of
|
||||
// a peer. If there is no notable state, a - is returned.
|
||||
func peerStatus(peer *ipnstate.PeerStatus) string {
|
||||
if !peer.Active {
|
||||
if peer.ExitNode {
|
||||
return "selected but offline"
|
||||
}
|
||||
if !peer.Online {
|
||||
return "offline"
|
||||
}
|
||||
}
|
||||
|
||||
if peer.ExitNode {
|
||||
return "selected"
|
||||
}
|
||||
|
||||
return "-"
|
||||
}
|
||||
|
||||
type filteredExitNodes struct {
|
||||
Countries []*filteredCountry
|
||||
}
|
||||
|
||||
type filteredCountry struct {
|
||||
Name string
|
||||
Cities []*filteredCity
|
||||
}
|
||||
|
||||
type filteredCity struct {
|
||||
Name string
|
||||
Peers []*ipnstate.PeerStatus
|
||||
}
|
||||
|
||||
const noLocationData = "-"
|
||||
|
||||
// filterFormatAndSortExitNodes filters and sorts exit nodes into
|
||||
// alphabetical order, by country, city and then by priority if
|
||||
// present.
|
||||
// If an exit node has location data, and the country has more than
|
||||
// once city, an `Any` city is added to the country that contains the
|
||||
// highest priority exit node within that country.
|
||||
// For exit nodes without location data, their country fields are
|
||||
// defined as '-' to indicate that the data is not available.
|
||||
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
|
||||
countries := make(map[string]*filteredCountry)
|
||||
cities := make(map[string]*filteredCity)
|
||||
for _, ps := range peers {
|
||||
if ps.Location == nil {
|
||||
ps.Location = &tailcfg.Location{
|
||||
Country: noLocationData,
|
||||
CountryCode: noLocationData,
|
||||
City: noLocationData,
|
||||
CityCode: noLocationData,
|
||||
}
|
||||
}
|
||||
|
||||
if filterBy != "" && ps.Location.Country != filterBy {
|
||||
continue
|
||||
}
|
||||
|
||||
co, coOK := countries[ps.Location.CountryCode]
|
||||
if !coOK {
|
||||
co = &filteredCountry{
|
||||
Name: ps.Location.Country,
|
||||
}
|
||||
countries[ps.Location.CountryCode] = co
|
||||
|
||||
}
|
||||
|
||||
ci, ciOK := cities[ps.Location.CityCode]
|
||||
if !ciOK {
|
||||
ci = &filteredCity{
|
||||
Name: ps.Location.City,
|
||||
}
|
||||
cities[ps.Location.CityCode] = ci
|
||||
co.Cities = append(co.Cities, ci)
|
||||
}
|
||||
ci.Peers = append(ci.Peers, ps)
|
||||
}
|
||||
|
||||
filteredExitNodes := filteredExitNodes{
|
||||
Countries: maps.Values(countries),
|
||||
}
|
||||
|
||||
for _, country := range filteredExitNodes.Countries {
|
||||
if country.Name == noLocationData {
|
||||
// Countries without location data should not
|
||||
// be filtered further.
|
||||
continue
|
||||
}
|
||||
|
||||
var countryANYPeer []*ipnstate.PeerStatus
|
||||
for _, city := range country.Cities {
|
||||
sortPeersByPriority(city.Peers)
|
||||
countryANYPeer = append(countryANYPeer, city.Peers...)
|
||||
var reducedCityPeers []*ipnstate.PeerStatus
|
||||
for i, peer := range city.Peers {
|
||||
if i == 0 || peer.ExitNode {
|
||||
// We only return the highest priority peer and any peer that
|
||||
// is currently the active exit node.
|
||||
reducedCityPeers = append(reducedCityPeers, peer)
|
||||
}
|
||||
}
|
||||
city.Peers = reducedCityPeers
|
||||
}
|
||||
sortByCityName(country.Cities)
|
||||
sortPeersByPriority(countryANYPeer)
|
||||
|
||||
if len(country.Cities) > 1 {
|
||||
// For countries with more than one city, we want to return the
|
||||
// option of the best peer for that country.
|
||||
country.Cities = append([]*filteredCity{
|
||||
{
|
||||
Name: "Any",
|
||||
Peers: []*ipnstate.PeerStatus{countryANYPeer[0]},
|
||||
},
|
||||
}, country.Cities...)
|
||||
}
|
||||
}
|
||||
sortByCountryName(filteredExitNodes.Countries)
|
||||
|
||||
return filteredExitNodes
|
||||
}
|
||||
|
||||
// sortPeersByPriority sorts a slice of PeerStatus
|
||||
// by location.Priority, in order of highest priority.
|
||||
func sortPeersByPriority(peers []*ipnstate.PeerStatus) {
|
||||
slices.SortStableFunc(peers, func(a, b *ipnstate.PeerStatus) int {
|
||||
return cmpx.Compare(b.Location.Priority, a.Location.Priority)
|
||||
})
|
||||
}
|
||||
|
||||
// sortByCityName sorts a slice of filteredCity alphabetically
|
||||
// by name. The '-' used to indicate no location data will always
|
||||
// be sorted to the front of the slice.
|
||||
func sortByCityName(cities []*filteredCity) {
|
||||
slices.SortStableFunc(cities, func(a, b *filteredCity) int { return strings.Compare(a.Name, b.Name) })
|
||||
}
|
||||
|
||||
// sortByCountryName sorts a slice of filteredCountry alphabetically
|
||||
// by name. The '-' used to indicate no location data will always
|
||||
// be sorted to the front of the slice.
|
||||
func sortByCountryName(countries []*filteredCountry) {
|
||||
slices.SortStableFunc(countries, func(a, b *filteredCountry) int { return strings.Compare(a.Name, b.Name) })
|
||||
}
|
||||
308
cmd/tailscale/cli/exitnode_test.go
Normal file
308
cmd/tailscale/cli/exitnode_test.go
Normal file
@@ -0,0 +1,308 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestFilterFormatAndSortExitNodes(t *testing.T) {
|
||||
t.Run("without filter", func(t *testing.T) {
|
||||
ps := []*ipnstate.PeerStatus{
|
||||
{
|
||||
HostName: "everest-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Everest",
|
||||
CountryCode: "evr",
|
||||
City: "Hillary",
|
||||
CityCode: "hil",
|
||||
Priority: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "lhotse-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Lhotse",
|
||||
CountryCode: "lho",
|
||||
City: "Fritz",
|
||||
CityCode: "fri",
|
||||
Priority: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "lhotse-2",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Lhotse",
|
||||
CountryCode: "lho",
|
||||
City: "Fritz",
|
||||
CityCode: "fri",
|
||||
Priority: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "nuptse-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Nuptse",
|
||||
CountryCode: "nup",
|
||||
City: "Walmsley",
|
||||
CityCode: "wal",
|
||||
Priority: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "nuptse-2",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Nuptse",
|
||||
CountryCode: "nup",
|
||||
City: "Bonington",
|
||||
CityCode: "bon",
|
||||
Priority: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "Makalu",
|
||||
},
|
||||
}
|
||||
|
||||
want := filteredExitNodes{
|
||||
Countries: []*filteredCountry{
|
||||
{
|
||||
Name: noLocationData,
|
||||
Cities: []*filteredCity{
|
||||
{
|
||||
Name: noLocationData,
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[5],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Everest",
|
||||
Cities: []*filteredCity{
|
||||
{
|
||||
Name: "Hillary",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Lhotse",
|
||||
Cities: []*filteredCity{
|
||||
{
|
||||
Name: "Fritz",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Nuptse",
|
||||
Cities: []*filteredCity{
|
||||
{
|
||||
Name: "Any",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[3],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Bonington",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[4],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Walmsley",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[3],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := filterFormatAndSortExitNodes(ps, "")
|
||||
|
||||
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
|
||||
t.Fatalf(res)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with country filter", func(t *testing.T) {
|
||||
ps := []*ipnstate.PeerStatus{
|
||||
{
|
||||
HostName: "baker-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Pacific",
|
||||
CountryCode: "pst",
|
||||
City: "Baker",
|
||||
CityCode: "col",
|
||||
Priority: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "hood-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Pacific",
|
||||
CountryCode: "pst",
|
||||
City: "Hood",
|
||||
CityCode: "hoo",
|
||||
Priority: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "rainier-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Pacific",
|
||||
CountryCode: "pst",
|
||||
City: "Rainier",
|
||||
CityCode: "rai",
|
||||
Priority: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "rainier-2",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Pacific",
|
||||
CountryCode: "pst",
|
||||
City: "Rainier",
|
||||
CityCode: "rai",
|
||||
Priority: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
HostName: "mitchell-1",
|
||||
Location: &tailcfg.Location{
|
||||
Country: "Atlantic",
|
||||
CountryCode: "atl",
|
||||
City: "Mitchell",
|
||||
CityCode: "mit",
|
||||
Priority: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
want := filteredExitNodes{
|
||||
Countries: []*filteredCountry{
|
||||
{
|
||||
Name: "Pacific",
|
||||
Cities: []*filteredCity{
|
||||
{
|
||||
Name: "Any",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Baker",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Hood",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Rainier",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[2],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := filterFormatAndSortExitNodes(ps, "Pacific")
|
||||
|
||||
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
|
||||
t.Fatalf(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSortPeersByPriority(t *testing.T) {
|
||||
ps := []*ipnstate.PeerStatus{
|
||||
{
|
||||
Location: &tailcfg.Location{
|
||||
Priority: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Location: &tailcfg.Location{
|
||||
Priority: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
Location: &tailcfg.Location{
|
||||
Priority: 300,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sortPeersByPriority(ps)
|
||||
|
||||
if ps[0].Location.Priority != 300 {
|
||||
t.Fatalf("sortPeersByPriority did not order PeerStatus with highest priority as index 0, got %v, want %v", ps[0].Location.Priority, 300)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortByCountryName(t *testing.T) {
|
||||
fc := []*filteredCountry{
|
||||
{
|
||||
Name: "Albania",
|
||||
},
|
||||
{
|
||||
Name: "Sweden",
|
||||
},
|
||||
{
|
||||
Name: "Zimbabwe",
|
||||
},
|
||||
{
|
||||
Name: noLocationData,
|
||||
},
|
||||
}
|
||||
|
||||
sortByCountryName(fc)
|
||||
|
||||
if fc[0].Name != noLocationData {
|
||||
t.Fatalf("sortByCountryName did not order countries by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortByCityName(t *testing.T) {
|
||||
fc := []*filteredCity{
|
||||
{
|
||||
Name: "Kingston",
|
||||
},
|
||||
{
|
||||
Name: "Goteborg",
|
||||
},
|
||||
{
|
||||
Name: "Squamish",
|
||||
},
|
||||
{
|
||||
Name: noLocationData,
|
||||
},
|
||||
}
|
||||
|
||||
sortByCityName(fc)
|
||||
|
||||
if fc[0].Name != noLocationData {
|
||||
t.Fatalf("sortByCityName did not order cities by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
|
||||
}
|
||||
}
|
||||
138
cmd/tailscale/cli/funnel.go
Normal file
138
cmd/tailscale/cli/funnel.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
||||
|
||||
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
||||
// The funnel subcommand is used to turn on/off the Funnel service.
|
||||
// Funnel is off by default.
|
||||
// Funnel allows you to publish a 'tailscale serve' server publicly, open to the
|
||||
// entire internet.
|
||||
// newFunnelCommand shares the same serveEnv as the "serve" subcommand. See
|
||||
// newServeCommand and serve.go for more details.
|
||||
func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "funnel",
|
||||
ShortHelp: "Turn on/off Funnel service",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"funnel <serve-port> {on|off}",
|
||||
"funnel status [--json]",
|
||||
}, "\n "),
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
"",
|
||||
"Turning off Funnel only turns off serving to the internet.",
|
||||
"It does not affect serving to your tailnet.",
|
||||
}, "\n"),
|
||||
Exec: e.runFunnel,
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// runFunnel is the entry point for the "tailscale funnel" subcommand and
|
||||
// manages turning on/off funnel. Funnel is off by default.
|
||||
//
|
||||
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
|
||||
func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
var on bool
|
||||
switch args[1] {
|
||||
case "on", "off":
|
||||
on = args[1] == "on"
|
||||
default:
|
||||
return flag.ErrHelp
|
||||
}
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port := uint16(port64)
|
||||
|
||||
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
printFunnelWarning(sc)
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
if on {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else {
|
||||
delete(sc.AllowFunnel, hp)
|
||||
// clear map mostly for testing
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
}
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
printFunnelWarning(sc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// printFunnelWarning prints a warning if the Funnel is on but there is no serve
|
||||
// config for its host:port.
|
||||
func printFunnelWarning(sc *ipn.ServeConfig) {
|
||||
var warn bool
|
||||
for hp, a := range sc.AllowFunnel {
|
||||
if !a {
|
||||
continue
|
||||
}
|
||||
_, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||
warn = true
|
||||
fmt.Fprintf(os.Stderr, "Warning: funnel=on for %s, but no serve config\n", hp)
|
||||
}
|
||||
}
|
||||
if warn {
|
||||
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -45,9 +46,15 @@ var netcheckArgs struct {
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
logf := logger.WithPrefix(log.Printf, "portmap: ")
|
||||
netMon, err := netmon.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
|
||||
PortMapper: portmapper.NewClient(logf, netMon, nil, nil),
|
||||
UseDNSCache: false, // always resolve, don't cache
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
@@ -96,7 +103,6 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
var err error
|
||||
switch netcheckArgs.format {
|
||||
case "":
|
||||
break
|
||||
case "json":
|
||||
j, err = json.MarshalIndent(report, "", "\t")
|
||||
case "json-line":
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
var netlockCmd = &ffcli.Command{
|
||||
@@ -39,8 +41,18 @@ var netlockCmd = &ffcli.Command{
|
||||
nlDisablementKDFCmd,
|
||||
nlLogCmd,
|
||||
nlLocalDisableCmd,
|
||||
nlRevokeKeysCmd,
|
||||
},
|
||||
Exec: runNetworkLockStatus,
|
||||
Exec: runNetworkLockNoSubcommand,
|
||||
}
|
||||
|
||||
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
|
||||
// Detect & handle the deprecated command 'lock tskey-wrap'.
|
||||
if len(args) >= 2 && args[0] == "tskey-wrap" {
|
||||
return runTskeyWrapCmd(ctx, args[1:])
|
||||
}
|
||||
|
||||
return runNetworkLockStatus(ctx, args)
|
||||
}
|
||||
|
||||
var nlInitArgs struct {
|
||||
@@ -230,6 +242,15 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
if k.Key == st.PublicKey {
|
||||
line.WriteString("(self)")
|
||||
}
|
||||
if k.Metadata["purpose"] == "pre-auth key" {
|
||||
if preauthKeyID := k.Metadata["authkey_stableid"]; preauthKeyID != "" {
|
||||
line.WriteString("(pre-auth key ")
|
||||
line.WriteString(preauthKeyID)
|
||||
line.WriteString(")")
|
||||
} else {
|
||||
line.WriteString("(pre-auth key)")
|
||||
}
|
||||
}
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
@@ -245,11 +266,13 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
for i, addr := range p.TailscaleIPs {
|
||||
line.WriteString(addr.String())
|
||||
if i < len(p.TailscaleIPs)-1 {
|
||||
line.WriteString(", ")
|
||||
line.WriteString(",")
|
||||
}
|
||||
}
|
||||
line.WriteString("\t")
|
||||
line.WriteString(string(p.StableID))
|
||||
line.WriteString("\t")
|
||||
line.WriteString(p.NodeKey.String())
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
@@ -267,14 +290,78 @@ var nlAddCmd = &ffcli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var nlRemoveArgs struct {
|
||||
resign bool
|
||||
}
|
||||
|
||||
var nlRemoveCmd = &ffcli.Command{
|
||||
Name: "remove",
|
||||
ShortUsage: "remove <public-key>...",
|
||||
ShortUsage: "remove [--re-sign=false] <public-key>...",
|
||||
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, nil, args)
|
||||
},
|
||||
Exec: runNetworkLockRemove,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock remove")
|
||||
fs.BoolVar(&nlRemoveArgs.resign, "re-sign", true, "resign signatures which would be invalidated by removal of trusted signing keys")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func runNetworkLockRemove(ctx context.Context, args []string) error {
|
||||
removeKeys, _, err := parseNLArgs(args, true, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, err := localClient.NetworkLockStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if !st.Enabled {
|
||||
return errors.New("tailnet lock is not enabled")
|
||||
}
|
||||
|
||||
if nlRemoveArgs.resign {
|
||||
// Validate we are not removing trust in ourselves while resigning. This is because
|
||||
// we resign with our own key, so the signatures would be immediately invalid.
|
||||
for _, k := range removeKeys {
|
||||
kID, err := k.ID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing KeyID for key %v: %w", k, err)
|
||||
}
|
||||
if bytes.Equal(st.PublicKey.KeyID(), kID) {
|
||||
return errors.New("cannot remove local trusted signing key while resigning; run command on a different node or with --re-sign=false")
|
||||
}
|
||||
}
|
||||
|
||||
// Resign affected signatures for each of the keys we are removing.
|
||||
for _, k := range removeKeys {
|
||||
kID, _ := k.ID() // err already checked above
|
||||
sigs, err := localClient.NetworkLockAffectedSigs(ctx, kID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("affected sigs for key %X: %w", kID, err)
|
||||
}
|
||||
|
||||
for _, sigBytes := range sigs {
|
||||
var sig tka.NodeKeySignature
|
||||
if err := sig.Unserialize(sigBytes); err != nil {
|
||||
return fmt.Errorf("failed decoding signature: %w", err)
|
||||
}
|
||||
var nodeKey key.NodePublic
|
||||
if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
|
||||
return fmt.Errorf("failed decoding pubkey for signature: %w", err)
|
||||
}
|
||||
|
||||
// Safety: NetworkLockAffectedSigs() verifies all signatures before
|
||||
// successfully returning.
|
||||
rotationKey, _ := sig.UnverifiedWrappingPublic()
|
||||
if err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey)); err != nil {
|
||||
return fmt.Errorf("failed to sign %v: %w", nodeKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return localClient.NetworkLockModify(ctx, nil, removeKeys)
|
||||
}
|
||||
|
||||
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
|
||||
@@ -350,13 +437,19 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
|
||||
var nlSignCmd = &ffcli.Command{
|
||||
Name: "sign",
|
||||
ShortUsage: "sign <node-key> [<rotation-key>]",
|
||||
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
LongHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
Exec: runNetworkLockSign,
|
||||
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
|
||||
ShortHelp: "Signs a node or pre-approved auth key",
|
||||
LongHelp: `Either:
|
||||
- signs a node key and transmits the signature to the coordination server, or
|
||||
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
|
||||
Exec: runNetworkLockSign,
|
||||
}
|
||||
|
||||
func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
|
||||
return runTskeyWrapCmd(ctx, args)
|
||||
}
|
||||
|
||||
var (
|
||||
nodeKey key.NodePublic
|
||||
rotationKey key.NLPublic
|
||||
@@ -374,7 +467,16 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||
err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||
// Provide a better help message for when someone clicks through the signing flow
|
||||
// on the wrong device.
|
||||
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
|
||||
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var nlDisableCmd = &ffcli.Command{
|
||||
@@ -558,3 +660,167 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runTskeyWrapCmd(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
|
||||
}
|
||||
if strings.Contains(args[0], "--TL") {
|
||||
return errors.New("Error: provided key was already wrapped")
|
||||
}
|
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
|
||||
return wrapAuthKey(ctx, args[0], st)
|
||||
}
|
||||
|
||||
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
|
||||
// Generate a separate tailnet-lock key just for the credential signature.
|
||||
// We use the free-form meta strings to mark a little bit of metadata about this
|
||||
// key.
|
||||
priv := key.NewNLPrivate()
|
||||
m := map[string]string{
|
||||
"purpose": "pre-auth key",
|
||||
"wrapper_stableid": string(status.Self.ID),
|
||||
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
|
||||
}
|
||||
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
|
||||
// We don't want to accidentally embed the nonce part of the authkey in
|
||||
// the event the format changes. As such, we make sure its in the format we
|
||||
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
|
||||
// out and embed the stableID.
|
||||
s := strings.TrimPrefix(keyStr, "tskey-auth-")
|
||||
m["authkey_stableid"] = s[:strings.Index(s, "-")]
|
||||
}
|
||||
k := tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Public: priv.Public().Verifier(),
|
||||
Votes: 1,
|
||||
Meta: m,
|
||||
}
|
||||
|
||||
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrapping failed: %w", err)
|
||||
}
|
||||
if err := localClient.NetworkLockModify(ctx, []tka.Key{k}, nil); err != nil {
|
||||
return fmt.Errorf("add key failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(wrapped)
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlRevokeKeysArgs struct {
|
||||
cosign bool
|
||||
finish bool
|
||||
forkFrom string
|
||||
}
|
||||
|
||||
var nlRevokeKeysCmd = &ffcli.Command{
|
||||
Name: "revoke-keys",
|
||||
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
|
||||
ShortHelp: "Revoke compromised tailnet-lock keys",
|
||||
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).
|
||||
|
||||
Revoked keys are prevented from being used in the future. Any nodes previously signed
|
||||
by revoked keys lose their authorization and must be signed again.
|
||||
|
||||
Revocation is a multi-step process that requires several signing nodes to ` + "`--cosign`" + ` the revocation. Use ` + "`tailscale lock remove`" + ` instead if the key has not been compromised.
|
||||
|
||||
1. To start, run ` + "`tailscale revoke-keys <tlpub-keys>`" + ` with the tailnet lock keys to revoke.
|
||||
2. Re-run the ` + "`--cosign`" + ` command output by ` + "`revoke-keys`" + ` on other signing nodes. Use the
|
||||
most recent command output on the next signing node in sequence.
|
||||
3. Once the number of ` + "`--cosign`" + `s is greater than the number of keys being revoked,
|
||||
run the command one final time with ` + "`--finish`" + ` instead of ` + "`--cosign`" + `.`,
|
||||
Exec: runNetworkLockRevokeKeys,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock revoke-keys")
|
||||
fs.BoolVar(&nlRevokeKeysArgs.cosign, "cosign", false, "continue generating the recovery using the tailnet lock key on this device and the provided recovery blob")
|
||||
fs.BoolVar(&nlRevokeKeysArgs.finish, "finish", false, "finish the recovery process by transmitting the revocation")
|
||||
fs.StringVar(&nlRevokeKeysArgs.forkFrom, "fork-from", "", "parent AUM hash to rewrite from (advanced users only)")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
|
||||
// First step in the process
|
||||
if !nlRevokeKeysArgs.cosign && !nlRevokeKeysArgs.finish {
|
||||
removeKeys, _, err := parseNLArgs(args, true, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyIDs := make([]tkatype.KeyID, len(removeKeys))
|
||||
for i, k := range removeKeys {
|
||||
keyIDs[i], err = k.ID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating keyID: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var forkFrom tka.AUMHash
|
||||
if nlRevokeKeysArgs.forkFrom != "" {
|
||||
if len(nlRevokeKeysArgs.forkFrom) == (len(forkFrom) * 2) {
|
||||
// Hex-encoded: like the output of the lock log command.
|
||||
b, err := hex.DecodeString(nlRevokeKeysArgs.forkFrom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid fork-from hash: %v", err)
|
||||
}
|
||||
copy(forkFrom[:], b)
|
||||
} else {
|
||||
if err := forkFrom.UnmarshalText([]byte(nlRevokeKeysArgs.forkFrom)); err != nil {
|
||||
return fmt.Errorf("invalid fork-from hash: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aumBytes, err := localClient.NetworkLockGenRecoveryAUM(ctx, keyIDs, forkFrom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generation of recovery AUM failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(`Run the following command on another machine with a trusted tailnet lock key:
|
||||
%s lock recover-compromised-key --cosign %X
|
||||
`, os.Args[0], aumBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we got this far, we need to co-sign the AUM and/or transmit it for distribution.
|
||||
b, err := hex.DecodeString(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing hex: %v", err)
|
||||
}
|
||||
var recoveryAUM tka.AUM
|
||||
if err := recoveryAUM.Unserialize(b); err != nil {
|
||||
return fmt.Errorf("decoding recovery AUM: %v", err)
|
||||
}
|
||||
|
||||
if nlRevokeKeysArgs.cosign {
|
||||
aumBytes, err := localClient.NetworkLockCosignRecoveryAUM(ctx, recoveryAUM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("co-signing recovery AUM failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(`Co-signing completed successfully.
|
||||
|
||||
To accumulate an additional signature, run the following command on another machine with a trusted tailnet lock key:
|
||||
%s lock recover-compromised-key --cosign %X
|
||||
|
||||
Alternatively if you are done with co-signing, complete recovery by running the following command:
|
||||
%s lock recover-compromised-key --finish %X
|
||||
`, os.Args[0], aumBytes, os.Args[0], aumBytes)
|
||||
}
|
||||
|
||||
if nlRevokeKeysArgs.finish {
|
||||
if err := localClient.NetworkLockSubmitRecoveryAUM(ctx, recoveryAUM); err != nil {
|
||||
return fmt.Errorf("submitting recovery AUM failed: %w", err)
|
||||
}
|
||||
fmt.Println("Recovery completed.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ relay node.
|
||||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
|
||||
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
|
||||
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send. 0 for infinity.")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
})(),
|
||||
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -35,78 +35,72 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
|
||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
serve [flags] <mount-point> {proxy|path|text} <arg>
|
||||
serve [flags] <sub-command> [sub-flags] <args>`),
|
||||
ShortHelp: "Serve content and local servers",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"serve http:<port> <mount-point> <source> [off]",
|
||||
"serve https:<port> <mount-point> <source> [off]",
|
||||
"serve tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve status [--json]",
|
||||
"serve reset",
|
||||
}, "\n "),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** ALPHA; all of this is subject to change ***
|
||||
*** BETA; all of this is subject to change ***
|
||||
|
||||
The 'tailscale serve' set of commands allows you to serve
|
||||
content and local servers from your Tailscale node to
|
||||
your tailnet.
|
||||
your tailnet.
|
||||
|
||||
You can also choose to enable the Tailscale Funnel with:
|
||||
'tailscale serve funnel on'. Funnel allows you to publish
|
||||
'tailscale funnel on'. Funnel allows you to publish
|
||||
a 'tailscale serve' server publicly, open to the entire
|
||||
internet. See https://tailscale.com/funnel.
|
||||
|
||||
EXAMPLES
|
||||
- To proxy requests to a web server at 127.0.0.1:3000:
|
||||
$ tailscale serve / proxy 3000
|
||||
$ tailscale serve https:443 / http://127.0.0.1:3000
|
||||
|
||||
Or, using the default port (443):
|
||||
$ tailscale serve https / http://127.0.0.1:3000
|
||||
|
||||
- To serve a single file or a directory of files:
|
||||
$ tailscale serve / path /home/alice/blog/index.html
|
||||
$ tailscale serve /images/ path /home/alice/blog/images
|
||||
$ tailscale serve https / /home/alice/blog/index.html
|
||||
$ tailscale serve https /images/ /home/alice/blog/images
|
||||
|
||||
- To serve simple static text:
|
||||
$ tailscale serve / text "Hello, world!"
|
||||
$ tailscale serve https:8080 / text:"Hello, world!"
|
||||
|
||||
- To serve over HTTP (tailnet only):
|
||||
$ tailscale serve http:80 / http://127.0.0.1:3000
|
||||
|
||||
Or, using the default port (80):
|
||||
$ tailscale serve http / http://127.0.0.1:3000
|
||||
|
||||
- To forward incoming TCP connections on port 2222 to a local TCP server on
|
||||
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
|
||||
$ tailscale serve tcp:2222 tcp://localhost:22
|
||||
|
||||
- To accept TCP TLS connections (terminated within tailscaled) proxied to a
|
||||
local plaintext server on port 80:
|
||||
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||
`),
|
||||
Exec: e.runServe,
|
||||
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
|
||||
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
|
||||
}),
|
||||
Exec: e.runServe,
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve status",
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "tcp",
|
||||
Exec: e.runServeTCP,
|
||||
ShortHelp: "add or remove a TCP port forward",
|
||||
LongHelp: strings.Join([]string{
|
||||
"EXAMPLES",
|
||||
" - Forward TLS over TCP to a local TCP server on port 5432:",
|
||||
" $ tailscale serve tcp 5432",
|
||||
"",
|
||||
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
|
||||
" $ tailscale serve tcp --terminate-tls 5432",
|
||||
}, "\n"),
|
||||
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "funnel",
|
||||
Exec: e.runServeFunnel,
|
||||
ShortUsage: "funnel [flags] {on|off}",
|
||||
ShortHelp: "turn Tailscale Funnel on or off",
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
"",
|
||||
"Turning off Funnel only turns off serving to the internet.",
|
||||
"It does not affect serving to your tailnet.",
|
||||
}, "\n"),
|
||||
Name: "reset",
|
||||
Exec: e.runServeReset,
|
||||
ShortHelp: "reset current serve/funnel config",
|
||||
FlagSet: e.newFlags("serve-reset", nil),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
@@ -135,6 +129,7 @@ type localServeClient interface {
|
||||
Status(context.Context) (*ipnstate.Status, error)
|
||||
GetServeConfig(context.Context) (*ipn.ServeConfig, error)
|
||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||
QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error)
|
||||
}
|
||||
|
||||
// serveEnv is the environment the serve command runs within. All I/O should be
|
||||
@@ -145,10 +140,7 @@ type localServeClient interface {
|
||||
// It also contains the flags, as registered with newServeCommand.
|
||||
type serveEnv struct {
|
||||
// flags
|
||||
servePort uint // Port to serve on. Defaults to 443.
|
||||
terminateTLS bool
|
||||
remove bool // remove a serve config
|
||||
json bool // output JSON (status only for now)
|
||||
json bool // output JSON (status only for now)
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
@@ -188,28 +180,16 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// validateServePort returns --serve-port flag value,
|
||||
// or an error if the port is not a valid port to serve on.
|
||||
func (e *serveEnv) validateServePort() (port uint16, err error) {
|
||||
// make sure e.servePort is uint16
|
||||
port = uint16(e.servePort)
|
||||
if uint(port) != e.servePort {
|
||||
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
|
||||
}
|
||||
// make sure e.servePort is 443, 8443 or 10000
|
||||
if port != 443 && port != 8443 && port != 10000 {
|
||||
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// runServe is the entry point for the "serve" subcommand, managing Web
|
||||
// serve config types like proxy, path, and text.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve / proxy 3000
|
||||
// - tailscale serve /images/ path /var/www/images/
|
||||
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
|
||||
// - tailscale serve http / http://localhost:3000
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https /images/ /var/www/images/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||
// - tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
@@ -229,39 +209,90 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
|
||||
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||
if !found {
|
||||
if srcType == "https" && srcPortStr == "" {
|
||||
// Default https port to 443.
|
||||
srcPortStr = "443"
|
||||
} else if srcType == "http" && srcPortStr == "" {
|
||||
// Default http port to 80.
|
||||
srcPortStr = "80"
|
||||
} else {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
}
|
||||
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
|
||||
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
srcPort, err := parseServePort(srcPortStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
|
||||
mount, err := cleanMountPoint(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid port %q: %w", srcPortStr, err)
|
||||
}
|
||||
|
||||
if e.remove {
|
||||
return e.handleWebServeRemove(ctx, mount)
|
||||
switch srcType {
|
||||
case "https", "http":
|
||||
mount, err := cleanMountPoint(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if turnOff {
|
||||
return e.handleWebServeRemove(ctx, srcPort, mount)
|
||||
}
|
||||
useTLS := srcType == "https"
|
||||
return e.handleWebServe(ctx, srcPort, useTLS, mount, args[2])
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
if turnOff {
|
||||
return e.handleTCPServeRemove(ctx, srcPort)
|
||||
}
|
||||
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
|
||||
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebServe handles the "tailscale serve (http/https):..." subcommand. It
|
||||
// configures the serve config to forward HTTPS connections to the given source.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve http / http://localhost:3000
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bool, mount, source string) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
|
||||
switch args[1] {
|
||||
case "path":
|
||||
ts, _, _ := strings.Cut(source, ":")
|
||||
switch {
|
||||
case ts == "text":
|
||||
text := strings.TrimPrefix(source, "text:")
|
||||
if text == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = text
|
||||
case isProxyTarget(source):
|
||||
t, err := expandProxyTarget(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
default: // assume path
|
||||
if version.IsSandboxedMacOS() {
|
||||
// don't allow path serving for now on macOS (2022-11-15)
|
||||
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
||||
}
|
||||
if !filepath.IsAbs(args[2]) {
|
||||
if !filepath.IsAbs(source) {
|
||||
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
fi, err := os.Stat(args[2])
|
||||
source = filepath.Clean(source)
|
||||
fi, err := os.Stat(source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
||||
return flag.ErrHelp
|
||||
@@ -271,21 +302,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
// for relative file links to work
|
||||
mount += "/"
|
||||
}
|
||||
h.Path = args[2]
|
||||
case "proxy":
|
||||
t, err := expandProxyTarget(args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
case "text":
|
||||
if args[2] == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = args[2]
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
|
||||
return flag.ErrHelp
|
||||
h.Path = source
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
@@ -300,14 +317,14 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
||||
|
||||
if _, ok := sc.Web[hp]; !ok {
|
||||
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
|
||||
@@ -339,12 +356,36 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
// isProxyTarget reports whether source is a valid proxy target.
|
||||
func isProxyTarget(source string) bool {
|
||||
if strings.HasPrefix(source, "http://") ||
|
||||
strings.HasPrefix(source, "https://") ||
|
||||
strings.HasPrefix(source, "https+insecure://") {
|
||||
return true
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
// support "localhost:3000", for example
|
||||
_, portStr, ok := strings.Cut(source, ":")
|
||||
if ok && allNumeric(portStr) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// allNumeric reports whether s only comprises of digits
|
||||
// and has at least one digit.
|
||||
func allNumeric(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s != ""
|
||||
}
|
||||
|
||||
// handleWebServeRemove removes a web handler from the serve config.
|
||||
// The srvPort argument is the serving port and the mount argument is
|
||||
// the mount point or registered path to remove.
|
||||
func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mount string) error {
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -359,9 +400,9 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if !sc.WebHandlerExists(hp, mount) {
|
||||
return errors.New("error: serve config does not exist")
|
||||
return errors.New("error: handler does not exist")
|
||||
}
|
||||
// delete existing handler, then cascade delete if empty
|
||||
delete(sc.Web[hp].Handlers, mount)
|
||||
@@ -386,6 +427,7 @@ func cleanMountPoint(mount string) (string, error) {
|
||||
if mount == "" {
|
||||
return "", errors.New("mount point cannot be empty")
|
||||
}
|
||||
mount = cleanMinGWPathConversionIfNeeded(mount)
|
||||
if !strings.HasPrefix(mount, "/") {
|
||||
mount = "/" + mount
|
||||
}
|
||||
@@ -396,18 +438,31 @@ func cleanMountPoint(mount string) (string, error) {
|
||||
return "", fmt.Errorf("invalid mount point %q", mount)
|
||||
}
|
||||
|
||||
func expandProxyTarget(target string) (string, error) {
|
||||
if allNumeric(target) {
|
||||
p, err := strconv.ParseUint(target, 10, 16)
|
||||
if p == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid port %q", target)
|
||||
}
|
||||
return "http://127.0.0.1:" + target, nil
|
||||
// cleanMinGWPathConversionIfNeeded strips the EXEPATH prefix from the given
|
||||
// path if the path is a MinGW(ish) (Windows) shell arg.
|
||||
//
|
||||
// MinGW(ish) (Windows) shells perform POSIX-to-Windows path conversion
|
||||
// converting the leading "/" of any shell arg to the EXEPATH, which mangles the
|
||||
// mount point. Strip the EXEPATH prefix if it exists. #7963
|
||||
//
|
||||
// "/C:/Program Files/Git/foo" -> "/foo"
|
||||
func cleanMinGWPathConversionIfNeeded(path string) string {
|
||||
// Only do this on Windows.
|
||||
if runtime.GOOS != "windows" {
|
||||
return path
|
||||
}
|
||||
if !strings.Contains(target, "://") {
|
||||
target = "http://" + target
|
||||
if _, ok := os.LookupEnv("MSYSTEM"); ok {
|
||||
exepath := filepath.ToSlash(os.Getenv("EXEPATH"))
|
||||
path = strings.TrimPrefix(path, exepath)
|
||||
}
|
||||
u, err := url.ParseRequestURI(target)
|
||||
return path
|
||||
}
|
||||
|
||||
func expandProxyTarget(source string) (string, error) {
|
||||
if !strings.Contains(source, "://") {
|
||||
source = "http://" + source
|
||||
}
|
||||
u, err := url.ParseRequestURI(source)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing url: %w", err)
|
||||
}
|
||||
@@ -417,9 +472,14 @@ func expandProxyTarget(target string) (string, error) {
|
||||
default:
|
||||
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
if port == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid port %q: %w", u.Port(), err)
|
||||
}
|
||||
|
||||
host := u.Hostname()
|
||||
switch host {
|
||||
// TODO(shayne,bradfitz): do we want to do this?
|
||||
case "localhost", "127.0.0.1":
|
||||
host = "127.0.0.1"
|
||||
default:
|
||||
@@ -429,19 +489,115 @@ func expandProxyTarget(target string) (string, error) {
|
||||
if u.Port() != "" {
|
||||
url += ":" + u.Port()
|
||||
}
|
||||
url += u.Path
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func allNumeric(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
// handleTCPServe handles the "tailscale serve tls-terminated-tcp:..." subcommand.
|
||||
// It configures the serve config to forward TCP connections to the
|
||||
// given source.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||
// - tailscale serve tls-terminated-tcp:8443 tcp://localhost:8080
|
||||
func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort uint16, dest string) error {
|
||||
var terminateTLS bool
|
||||
switch srcType {
|
||||
case "tcp":
|
||||
terminateTLS = false
|
||||
case "tls-terminated-tcp":
|
||||
terminateTLS = true
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
dstURL, err := url.Parse(dest)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
switch host {
|
||||
case "localhost", "127.0.0.1":
|
||||
// ok
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
|
||||
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if terminateTLS {
|
||||
sc.TCP[srcPort].TerminateTLS = dnsName
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s != ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runServeStatus prints the current serve config.
|
||||
// handleTCPServeRemove removes the TCP forwarding configuration for the
|
||||
// given srvPort, or serving port.
|
||||
func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
if sc.IsServingWeb(src) {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||
}
|
||||
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
||||
delete(sc.TCP, src)
|
||||
// clear map mostly for testing
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
|
||||
// runServeStatus is the entry point for the "serve status"
|
||||
// subcommand and prints the current serve config.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale status
|
||||
@@ -460,6 +616,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
e.stdout().Write(j)
|
||||
return nil
|
||||
}
|
||||
printFunnelStatus(ctx)
|
||||
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
|
||||
printf("No serve config\n")
|
||||
return nil
|
||||
@@ -475,20 +632,13 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
printf("\n")
|
||||
}
|
||||
for hp := range sc.Web {
|
||||
printWebStatusTree(sc, hp)
|
||||
err := e.printWebStatusTree(sc, hp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printf("\n")
|
||||
}
|
||||
// warn when funnel on without handlers
|
||||
for hp, a := range sc.AllowFunnel {
|
||||
if !a {
|
||||
continue
|
||||
}
|
||||
_, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
|
||||
}
|
||||
}
|
||||
printFunnelWarning(sc)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -524,20 +674,37 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
|
||||
return nil
|
||||
}
|
||||
|
||||
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
|
||||
func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error {
|
||||
// No-op if no serve config
|
||||
if sc == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
fStatus := "tailnet only"
|
||||
if sc.AllowFunnel[hp] {
|
||||
fStatus = "Funnel on"
|
||||
}
|
||||
host, portStr, _ := net.SplitHostPort(string(hp))
|
||||
if portStr == "443" {
|
||||
printf("https://%s (%s)\n", host, fStatus)
|
||||
} else {
|
||||
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
|
||||
|
||||
port, err := parseServePort(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(port) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
portPart := ":" + portStr
|
||||
if scheme == "http" && portStr == "80" ||
|
||||
scheme == "https" && portStr == "443" {
|
||||
portPart = ""
|
||||
}
|
||||
if scheme == "http" {
|
||||
hostname, _, _ := strings.Cut(host, ".")
|
||||
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
|
||||
}
|
||||
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
@@ -564,6 +731,8 @@ func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
|
||||
t, d := srvTypeAndDesc(h)
|
||||
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func elipticallyTruncate(s string, max int) string {
|
||||
@@ -573,151 +742,27 @@ func elipticallyTruncate(s string, max int) string {
|
||||
return s[:max-3] + "..."
|
||||
}
|
||||
|
||||
// runServeTCP is the entry point for the "serve tcp" subcommand and
|
||||
// manages the serve config for TCP forwarding.
|
||||
// runServeReset clears out the current serve config.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve tcp 5432
|
||||
// - tailscale serve --serve-port=8443 tcp 4430
|
||||
// - tailscale serve --serve-port=10000 tcp --terminate-tls 8080
|
||||
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
// Usage:
|
||||
// - tailscale serve reset
|
||||
func (e *serveEnv) runServeReset(ctx context.Context, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portStr := args[0]
|
||||
p, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if p == 0 || err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + portStr
|
||||
|
||||
if sc.IsServingWeb(srvPort) {
|
||||
if e.remove {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
|
||||
}
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
|
||||
}
|
||||
|
||||
if e.remove {
|
||||
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
|
||||
delete(sc.TCP, srvPort)
|
||||
// clear map mostly for testing
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.terminateTLS {
|
||||
sc.TCP[srvPort].TerminateTLS = dnsName
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
sc := new(ipn.ServeConfig)
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
// runServeFunnel is the entry point for the "serve funnel" subcommand and
|
||||
// manages turning on/off funnel. Funnel is off by default.
|
||||
//
|
||||
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
|
||||
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
// parseServePort parses a port number from a string and returns it as a
|
||||
// uint16. It returns an error if the port number is invalid or zero.
|
||||
func parseServePort(s string) (uint16, error) {
|
||||
p, err := strconv.ParseUint(s, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
|
||||
var on bool
|
||||
switch args[0] {
|
||||
case "on", "off":
|
||||
on = args[0] == "on"
|
||||
default:
|
||||
return flag.ErrHelp
|
||||
if p == 0 {
|
||||
return 0, errors.New("port number must be non-zero")
|
||||
}
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if err := checkHasAccess(st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
if on {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else {
|
||||
delete(sc.AllowFunnel, hp)
|
||||
// clear map mostly for testing
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
}
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHasAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func checkHasAccess(nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
|
||||
}
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
return uint16(p), nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -48,30 +49,6 @@ func TestCleanMountPoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHasAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := checkHasAccess(tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
continue
|
||||
case tt.wantErr:
|
||||
t.Fatalf("got no error, want error")
|
||||
case !tt.wantErr:
|
||||
t.Fatalf("got error %v, want no error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeConfigMutations(t *testing.T) {
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
@@ -80,6 +57,8 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
want *ipn.ServeConfig // non-nil means we want a save of this value
|
||||
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
||||
line int // line number of addStep call, for error messages
|
||||
|
||||
debugBreak func()
|
||||
}
|
||||
var steps []step
|
||||
add := func(s step) {
|
||||
@@ -90,19 +69,19 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
// funnel
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
@@ -112,28 +91,77 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{ // allow omitting port (default to 80)
|
||||
command: cmd("http / http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("http:9999 /abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy 0"), // invalid port, too low
|
||||
command: cmd("http:9999 /abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("http:8080 /abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy 65536"), // invalid port, too high
|
||||
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy somehost"), // invalid host
|
||||
command: cmd("https:443 / http://somehost:3000"), // invalid host
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy http://otherhost"), // invalid host
|
||||
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy 3000"),
|
||||
add(step{ // allow omitting port (default to 443)
|
||||
command: cmd("https / http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -143,12 +171,33 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // invalid port
|
||||
command: cmd("--serve-port=9999 /abc proxy 3001"),
|
||||
wantErr: anyErr(),
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("https:9999 /abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
||||
command: cmd("https:9999 /abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -162,7 +211,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=10000 / text hi"),
|
||||
command: cmd("https:10000 / text:hi"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
|
||||
@@ -180,12 +229,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /foo"),
|
||||
command: cmd("https:443 /foo off"),
|
||||
want: nil, // nothing to save
|
||||
wantErr: anyErr(),
|
||||
}) // handler doesn't exist, so we get an error
|
||||
add(step{
|
||||
command: cmd("--remove --serve-port=10000 /"),
|
||||
command: cmd("https:10000 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -199,7 +248,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -210,11 +259,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove --serve-port=8443 /abc"),
|
||||
command: cmd("https:8443 /abc off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||
add(step{ // clean mount: "bar" becomes "/bar"
|
||||
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -225,12 +274,15 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // try resetting using reset command
|
||||
command: cmd("reset"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
|
||||
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -242,7 +294,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/foo proxy localhost:3000"),
|
||||
command: cmd("https:443 /foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -253,7 +305,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // test a second handler on the same port
|
||||
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
|
||||
command: cmd("https:8443 /foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -266,19 +318,50 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // support path in proxy
|
||||
command: cmd("https / http://127.0.0.1:3000/foo/bar"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// tcp
|
||||
add(step{reset: true})
|
||||
add(step{ // must include scheme for tcp
|
||||
command: cmd("tls-terminated-tcp:443 localhost:5432"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // !somehost, must be localhost or 127.0.0.1
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too low
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too high
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:5432"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls 8443"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
@@ -289,11 +372,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls 8443"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp --terminate-tls 8444"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
@@ -304,35 +387,41 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls=false 8445"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:8445"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:8445",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("tcp 123"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:123"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:123",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove tcp 321"),
|
||||
add(step{ // handler doesn't exist, so we get an error
|
||||
command: cmd("tls-terminated-tcp:8443 off"),
|
||||
wantErr: anyErr(),
|
||||
}) // handler doesn't exist, so we get an error
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove tcp 123"),
|
||||
command: cmd("tls-terminated-tcp:443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// text
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ text hello"),
|
||||
command: cmd("https:443 / text:hello"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -353,7 +442,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
add(step{reset: true})
|
||||
writeFile("foo", "this is foo")
|
||||
add(step{
|
||||
command: cmd("/ path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 / " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -366,7 +455,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
||||
writeFile("subdir/file-a", "this is A")
|
||||
add(step{
|
||||
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
|
||||
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -377,13 +466,13 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ path missing"),
|
||||
add(step{ // bad path
|
||||
command: cmd("https:443 / bad/path"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -394,14 +483,14 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// combos
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -412,7 +501,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
@@ -424,7 +513,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // serving on secondary port doesn't change funnel
|
||||
command: cmd("--serve-port=8443 /bar proxy 3001"),
|
||||
command: cmd("https:8443 /bar localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -439,7 +528,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // turn funnel on for secondary port
|
||||
command: cmd("--serve-port=8443 funnel on"),
|
||||
command: cmd("funnel 8443 on"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -454,7 +543,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // turn funnel off for primary port 443
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -469,7 +558,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // remove secondary port
|
||||
command: cmd("--serve-port=8443 --remove /bar"),
|
||||
command: cmd("https:8443 /bar off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
@@ -481,7 +570,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // start a tcp forwarder on 8443
|
||||
command: cmd("--serve-port=8443 tcp 5432"),
|
||||
command: cmd("tcp:8443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
@@ -493,27 +582,27 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // remove primary port http handler
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
},
|
||||
})
|
||||
add(step{ // remove tcp forwarder
|
||||
command: cmd("--serve-port=8443 --remove tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:8443 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
},
|
||||
})
|
||||
add(step{ // turn off funnel
|
||||
command: cmd("--serve-port=8443 funnel off"),
|
||||
command: cmd("funnel 8443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// tricky steps
|
||||
add(step{reset: true})
|
||||
add(step{ // a directory with a trailing slash mount point
|
||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -524,7 +613,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -536,7 +625,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
})
|
||||
add(step{reset: true}) // reset and do the opposite
|
||||
add(step{ // a file without a trailing slash mount point
|
||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -547,7 +636,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -560,37 +649,24 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
|
||||
// error states
|
||||
add(step{reset: true})
|
||||
add(step{ // make sure we can't add "tcp" as if it was a mount
|
||||
command: cmd("tcp text foo"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // "/tcp" is fine though as a mount
|
||||
command: cmd("/tcp text foo"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/tcp": {Text: "foo"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // tcp forward 5432 on serve port 443
|
||||
command: cmd("tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:5432"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a web handler on the same port
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // start a web handler on port 443
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -600,14 +676,17 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
|
||||
command: cmd("tcp 5432"),
|
||||
add(step{ // try to start a tcp forwarder on the same serve port
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
|
||||
lc := &fakeLocalServeClient{}
|
||||
// And now run the steps above.
|
||||
for i, st := range steps {
|
||||
if st.debugBreak != nil {
|
||||
st.debugBreak()
|
||||
}
|
||||
if st.reset {
|
||||
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
||||
lc.config = nil
|
||||
@@ -625,8 +704,16 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
testStdout: &stdout,
|
||||
}
|
||||
lastCount := lc.setCount
|
||||
cmd := newServeCommand(e)
|
||||
err := cmd.ParseAndRun(context.Background(), st.command)
|
||||
var cmd *ffcli.Command
|
||||
var args []string
|
||||
if st.command[0] == "funnel" {
|
||||
cmd = newFunnelCommand(e)
|
||||
args = st.command[1:]
|
||||
} else {
|
||||
cmd = newServeCommand(e)
|
||||
args = st.command
|
||||
}
|
||||
err := cmd.ParseAndRun(context.Background(), args)
|
||||
if flagOut.Len() > 0 {
|
||||
t.Logf("flag package output: %q", flagOut.Bytes())
|
||||
}
|
||||
@@ -677,7 +764,7 @@ var fakeStatus = &ipnstate.Status{
|
||||
BackendState: ipn.Running.String(),
|
||||
Self: &ipnstate.PeerStatus{
|
||||
DNSName: "foo.test.ts.net",
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -695,6 +782,10 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// exactError returns an error checker that wants exactly the provided want error.
|
||||
// If optName is non-empty, it's used in the error message.
|
||||
func exactErr(want error, optName ...string) func(error) string {
|
||||
@@ -717,7 +808,5 @@ func anyErr() func(error) string {
|
||||
}
|
||||
|
||||
func cmd(s string) []string {
|
||||
cmds := strings.Fields(s)
|
||||
fmt.Printf("cmd: %v", cmds)
|
||||
return cmds
|
||||
return strings.Fields(s)
|
||||
}
|
||||
|
||||
@@ -200,6 +200,8 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
|
||||
locBasedExitNode := false
|
||||
if statusArgs.peers {
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
@@ -207,6 +209,12 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
if ps.Location != nil && ps.ExitNodeOption && !ps.ExitNode {
|
||||
// Location based exit nodes are only shown with the
|
||||
// `exit-node list` command.
|
||||
locBasedExitNode = true
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
ipnstate.SortPeers(peers)
|
||||
@@ -218,6 +226,10 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
Stdout.Write(buf.Bytes())
|
||||
if locBasedExitNode {
|
||||
println()
|
||||
println("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
|
||||
}
|
||||
if len(st.Health) > 0 {
|
||||
outln()
|
||||
printHealth()
|
||||
@@ -258,6 +270,7 @@ func printFunnelStatus(ctx context.Context) {
|
||||
}
|
||||
printf("# - %s\n", url)
|
||||
}
|
||||
outln()
|
||||
}
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
@@ -275,7 +288,7 @@ func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
}
|
||||
return s, false
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
return "Machine is not yet authorized by tailnet admin.", false
|
||||
return "Machine is not yet approved by tailnet admin.", false
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
return st.BackendState, true
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -26,6 +28,8 @@ import (
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -34,6 +38,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -320,8 +325,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
@@ -409,6 +414,12 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if env.upArgs.forceReauth && isSSHOverTailscale() {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will result in your SSH session disconnecting.`, env.upArgs.acceptedRisks); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
@@ -584,7 +595,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
@@ -656,6 +667,10 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := localClient.Start(ctx, ipn.Options{
|
||||
AuthKey: authKey,
|
||||
UpdatePrefs: prefs,
|
||||
@@ -710,7 +725,8 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
// the health check, rather than just a string.
|
||||
func upWorthyWarning(s string) bool {
|
||||
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
|
||||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff)
|
||||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
|
||||
strings.Contains(s, healthmsg.LockedOut)
|
||||
}
|
||||
|
||||
func checkUpWarnings(ctx context.Context) {
|
||||
@@ -1095,3 +1111,93 @@ func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
// client lives in the same repo as this code.
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
}
|
||||
|
||||
// resolveAuthKey either returns v unchanged (in the common case) or, if it
|
||||
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
|
||||
//
|
||||
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
|
||||
//
|
||||
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
|
||||
// property defaults to true if unspecified. The "preauthorized" defaults to
|
||||
// false. The "baseURL" defaults to https://api.tailscale.com.
|
||||
// The passed in tags are required, and must be non-empty. These will be
|
||||
// set on the authkey generated by the OAuth2 dance.
|
||||
func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
|
||||
if !strings.HasPrefix(v, "tskey-client-") {
|
||||
return v, nil
|
||||
}
|
||||
if tags == "" {
|
||||
return "", errors.New("oauth authkeys require --advertise-tags")
|
||||
}
|
||||
|
||||
clientSecret, named, _ := strings.Cut(v, "?")
|
||||
attrs, err := url.ParseQuery(named)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for k := range attrs {
|
||||
switch k {
|
||||
case "ephemeral", "preauthorized", "baseURL":
|
||||
default:
|
||||
return "", fmt.Errorf("unknown attribute %q", k)
|
||||
}
|
||||
}
|
||||
getBool := func(name string, def bool) (bool, error) {
|
||||
v := attrs.Get(name)
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
ret, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
ephemeral, err := getBool("ephemeral", true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
preauth, err := getBool("preauthorized", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseURL := "https://api.tailscale.com"
|
||||
if v := attrs.Get("baseURL"); v != "" {
|
||||
baseURL = v
|
||||
}
|
||||
|
||||
credentials := clientcredentials.Config{
|
||||
ClientID: "some-client-id", // ignored
|
||||
ClientSecret: clientSecret,
|
||||
TokenURL: baseURL + "/api/v2/oauth/token",
|
||||
Scopes: []string{"device"},
|
||||
}
|
||||
|
||||
tsClient := tailscale.NewClient("-", nil)
|
||||
tsClient.HTTPClient = credentials.Client(ctx)
|
||||
tsClient.BaseURL = baseURL
|
||||
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
Reusable: false,
|
||||
Ephemeral: ephemeral,
|
||||
Preauthorized: preauth,
|
||||
Tags: strings.Split(tags, ","),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return authkey, nil
|
||||
}
|
||||
|
||||
@@ -44,17 +44,27 @@ var updateCmd = &ffcli.Command{
|
||||
fs := newFlagSet("update")
|
||||
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts")
|
||||
fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts")
|
||||
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
|
||||
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
|
||||
fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)")
|
||||
// These flags are not supported on several systems that only provide
|
||||
// the latest version of Tailscale:
|
||||
//
|
||||
// - Arch (and other pacman-based distros)
|
||||
// - Alpine (and other apk-based distros)
|
||||
// - FreeBSD (and other pkg-based distros)
|
||||
if distro.Get() != distro.Arch && distro.Get() != distro.Alpine && runtime.GOOS != "freebsd" {
|
||||
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
|
||||
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
|
||||
}
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var updateArgs struct {
|
||||
yes bool
|
||||
dryRun bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
yes bool
|
||||
dryRun bool
|
||||
appStore bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
}
|
||||
|
||||
// winMSIEnv is the environment variable that, if set, is the MSI file for the
|
||||
@@ -137,19 +147,40 @@ func newUpdater() (*updater, error) {
|
||||
up.update = up.updateSynology
|
||||
case distro.Debian: // includes Ubuntu
|
||||
up.update = up.updateDebLike
|
||||
case distro.Arch:
|
||||
up.update = up.updateArchLike
|
||||
case distro.Alpine:
|
||||
up.update = up.updateAlpineLike
|
||||
}
|
||||
// TODO(awly): add support for Alpine
|
||||
switch {
|
||||
case haveExecutable("pacman"):
|
||||
up.update = up.updateArchLike
|
||||
case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
|
||||
// The distro.Debian switch case above should catch most apt-based
|
||||
// systems, but add this fallback just in case.
|
||||
up.update = up.updateDebLike
|
||||
case haveExecutable("dnf"):
|
||||
up.update = up.updateFedoraLike("dnf")
|
||||
case haveExecutable("yum"):
|
||||
up.update = up.updateFedoraLike("yum")
|
||||
case haveExecutable("apk"):
|
||||
up.update = up.updateAlpineLike
|
||||
}
|
||||
case "darwin":
|
||||
switch {
|
||||
case !version.IsSandboxedMacOS():
|
||||
case !updateArgs.appStore && !version.IsSandboxedMacOS():
|
||||
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
|
||||
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
case !updateArgs.appStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
up.update = up.updateMacSys
|
||||
default:
|
||||
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version")
|
||||
up.update = up.updateMacAppStore
|
||||
}
|
||||
case "freebsd":
|
||||
up.update = up.updateFreeBSD
|
||||
}
|
||||
if up.update == nil {
|
||||
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/")
|
||||
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
|
||||
}
|
||||
return up, nil
|
||||
}
|
||||
@@ -171,6 +202,8 @@ func (up *updater) currentOrDryRun(ver string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var errUserAborted = errors.New("aborting update")
|
||||
|
||||
func (up *updater) confirm(ver string) error {
|
||||
if updateArgs.yes {
|
||||
log.Printf("Updating Tailscale from %v to %v; --yes given, continuing without prompts.\n", version.Short(), ver)
|
||||
@@ -185,7 +218,7 @@ func (up *updater) confirm(ver string) error {
|
||||
case "y", "yes", "sure":
|
||||
return nil
|
||||
}
|
||||
return errors.New("aborting update")
|
||||
return errUserAborted
|
||||
}
|
||||
|
||||
func (up *updater) updateSynology() error {
|
||||
@@ -197,48 +230,22 @@ func (up *updater) updateSynology() error {
|
||||
}
|
||||
|
||||
func (up *updater) updateDebLike() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz"
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
f, ok := latest.Tarballs[runtime.GOARCH]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't update architecture %q", runtime.GOARCH)
|
||||
}
|
||||
ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_")
|
||||
if !ok {
|
||||
return fmt.Errorf("can't parse version from %q", f)
|
||||
}
|
||||
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
track := "unstable"
|
||||
if stable, ok := versionIsStable(ver); !ok {
|
||||
return fmt.Errorf("malformed version %q", ver)
|
||||
} else if stable {
|
||||
track = "stable"
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("must be root; use sudo")
|
||||
}
|
||||
|
||||
if updated, err := updateDebianAptSourcesList(track); err != nil {
|
||||
if updated, err := updateDebianAptSourcesList(up.track); err != nil {
|
||||
return err
|
||||
} else if updated {
|
||||
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, track)
|
||||
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, up.track)
|
||||
}
|
||||
|
||||
cmd := exec.Command("apt-get", "update",
|
||||
@@ -324,6 +331,204 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (up *updater) updateArchLike() (err error) {
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil && !errors.Is(err, errUserAborted) {
|
||||
err = fmt.Errorf(`%w; you can try updating using "pacman --sync --refresh tailscale"`, err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exec.Command("pacman", "--sync", "--refresh", "--info", "tailscale").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed checking pacman for latest tailscale version: %w, output: %q", err, out)
|
||||
}
|
||||
ver, err := parsePacmanVersion(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if err := up.confirm(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("pacman", "--sync", "--noconfirm", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using pacman: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePacmanVersion(out []byte) (string, error) {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
// The line we're looking for looks like this:
|
||||
// Version : 1.44.2-1
|
||||
if !strings.HasPrefix(line, "Version") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
|
||||
}
|
||||
ver := strings.TrimSpace(parts[1])
|
||||
// Trim the Arch patch version.
|
||||
ver = strings.Split(ver, "-")[0]
|
||||
if ver == "" {
|
||||
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
return "", fmt.Errorf("could not find latest version of tailscale via pacman")
|
||||
}
|
||||
|
||||
const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
|
||||
|
||||
// updateFedoraLike updates tailscale on any distros in the Fedora family,
|
||||
// specifically anything that uses "dnf" or "yum" package managers. The actual
|
||||
// package manager is passed via packageManager.
|
||||
func (up *updater) updateFedoraLike(packageManager string) func() error {
|
||||
return func() (err error) {
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && !errors.Is(err, errUserAborted) {
|
||||
err = fmt.Errorf(`%w; you can try updating using "%s upgrade tailscale"`, err, packageManager)
|
||||
}
|
||||
}()
|
||||
|
||||
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if err := up.confirm(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.track); err != nil {
|
||||
return err
|
||||
} else if updated {
|
||||
fmt.Printf("Updated %s to use the %s track\n", yumRepoConfigFile, up.track)
|
||||
}
|
||||
|
||||
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// updateYUMRepoTrack updates the repoFile file to make sure it has the
|
||||
// provided track (stable or unstable) in it.
|
||||
func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
|
||||
was, err := os.ReadFile(repoFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
|
||||
urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
|
||||
|
||||
s := bufio.NewScanner(bytes.NewReader(was))
|
||||
newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
// Handle repo section name, like "[tailscale-stable]".
|
||||
if len(line) > 0 && line[0] == '[' {
|
||||
if !strings.HasPrefix(line, "[tailscale-") {
|
||||
return false, fmt.Errorf("%q does not look like a tailscale repo file, it contains an unexpected %q section", repoFile, line)
|
||||
}
|
||||
fmt.Fprintf(newContent, "[tailscale-%s]\n", dstTrack)
|
||||
continue
|
||||
}
|
||||
// Update the track mentioned in repo name.
|
||||
if strings.HasPrefix(line, "name=") {
|
||||
fmt.Fprintf(newContent, "name=Tailscale %s\n", dstTrack)
|
||||
continue
|
||||
}
|
||||
// Update the actual repo URLs.
|
||||
if strings.HasPrefix(line, "baseurl=") || strings.HasPrefix(line, "gpgkey=") {
|
||||
fmt.Fprintln(newContent, urlRe.ReplaceAllString(line, urlReplacement))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(newContent, line)
|
||||
}
|
||||
if bytes.Equal(was, newContent.Bytes()) {
|
||||
return false, nil
|
||||
}
|
||||
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
|
||||
}
|
||||
|
||||
func (up *updater) updateAlpineLike() (err error) {
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil && !errors.Is(err, errUserAborted) {
|
||||
err = fmt.Errorf(`%w; you can try updating using "apk upgrade tailscale"`, err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exec.Command("apk", "update").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed refresh apk repository indexes: %w, output: %q", err, out)
|
||||
}
|
||||
out, err = exec.Command("apk", "info", "tailscale").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed checking apk for latest tailscale version: %w, output: %q", err, out)
|
||||
}
|
||||
ver, err := parseAlpinePackageVersion(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if err := up.confirm(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("apk", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using apk: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAlpinePackageVersion(out []byte) (string, error) {
|
||||
s := bufio.NewScanner(bytes.NewReader(out))
|
||||
for s.Scan() {
|
||||
// The line should look like this:
|
||||
// tailscale-1.44.2-r0 description:
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if !strings.HasPrefix(line, "tailscale-") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "-", 3)
|
||||
if len(parts) < 3 {
|
||||
return "", fmt.Errorf("malformed info line: %q", line)
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
return "", errors.New("tailscale version not found in output")
|
||||
}
|
||||
|
||||
func (up *updater) updateMacSys() error {
|
||||
// use sparkle? do we have permissions from this context? does sudo help?
|
||||
// We can at least fail with a command they can run to update from the shell.
|
||||
@@ -333,30 +538,68 @@ func (up *updater) updateMacSys() error {
|
||||
return errors.New("The 'update' command is not yet implemented on macOS.")
|
||||
}
|
||||
|
||||
func (up *updater) updateMacAppStore() error {
|
||||
out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out))
|
||||
}
|
||||
const on = "1\n"
|
||||
if string(out) != on {
|
||||
fmt.Fprintln(os.Stderr, "NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for ‘update’).")
|
||||
}
|
||||
|
||||
out, err = exec.Command("softwareupdate", "--list").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out))
|
||||
}
|
||||
|
||||
newTailscale := parseSoftwareupdateList(out)
|
||||
if newTailscale == "" {
|
||||
fmt.Println("no Tailscale update available")
|
||||
return nil
|
||||
}
|
||||
|
||||
newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-")
|
||||
if up.currentOrDryRun(newTailscaleVer) {
|
||||
return nil
|
||||
}
|
||||
if err := up.confirm(newTailscaleVer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var macOSAppStoreListPattern = regexp.MustCompile(`(?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+)`)
|
||||
|
||||
// parseSoftwareupdateList searches the output of `softwareupdate --list` on
|
||||
// Darwin and returns the matching Tailscale package label. If there is none,
|
||||
// returns the empty string.
|
||||
//
|
||||
// See TestParseSoftwareupdateList for example inputs.
|
||||
func parseSoftwareupdateList(stdout []byte) string {
|
||||
matches := macOSAppStoreListPattern.FindSubmatch(stdout)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return string(matches[1])
|
||||
}
|
||||
|
||||
var (
|
||||
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||
markTempFileFunc func(string) error // or nil on non-Windows
|
||||
)
|
||||
|
||||
func (up *updater) updateWindows() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Version string
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
ver = latest.Version
|
||||
if ver == "" {
|
||||
return errors.New("no version found")
|
||||
}
|
||||
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
@@ -585,3 +828,85 @@ func (pw *progressWriter) print() {
|
||||
pw.lastPrint = time.Now()
|
||||
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||
}
|
||||
|
||||
func (up *updater) updateFreeBSD() (err error) {
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil && !errors.Is(err, errUserAborted) {
|
||||
err = fmt.Errorf(`%w; you can try updating using "pkg upgrade tailscale"`, err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exec.Command("pkg", "update").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed refresh pkg repository indexes: %w, output: %q", err, out)
|
||||
}
|
||||
out, err = exec.Command("pkg", "rquery", "%v", "tailscale").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed checking pkg for latest tailscale version: %w, output: %q", err, out)
|
||||
}
|
||||
ver := string(bytes.TrimSpace(out))
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if err := up.confirm(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("pkg", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using pkg: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func haveExecutable(name string) bool {
|
||||
path, err := exec.LookPath(name)
|
||||
return err == nil && path != ""
|
||||
}
|
||||
|
||||
func requestedTailscaleVersion(ver, track string) (string, error) {
|
||||
if ver != "" {
|
||||
return ver, nil
|
||||
}
|
||||
return latestTailscaleVersion(track)
|
||||
}
|
||||
|
||||
func latestTailscaleVersion(track string) (string, error) {
|
||||
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetching latest tailscale version: %w", err)
|
||||
}
|
||||
var latest struct {
|
||||
Version string
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
if latest.Version == "" {
|
||||
return "", fmt.Errorf("no version found at %q", url)
|
||||
}
|
||||
return latest.Version, nil
|
||||
}
|
||||
|
||||
func requireRoot() error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return errors.New("must be root; use sudo")
|
||||
case "freebsd", "openbsd":
|
||||
return errors.New("must be root; use doas")
|
||||
default:
|
||||
return errors.New("must be root")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -73,3 +77,366 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSoftwareupdateList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "update-at-end-of-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
* Label: Tailscale-1.23.4
|
||||
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
||||
`),
|
||||
want: "Tailscale-1.23.4",
|
||||
},
|
||||
{
|
||||
name: "update-in-middle-of-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: Tailscale-1.23.5000
|
||||
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
`),
|
||||
want: "Tailscale-1.23.5000",
|
||||
},
|
||||
{
|
||||
name: "update-not-in-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
`),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "decoy-in-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: Malware-1.0
|
||||
Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
|
||||
`),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := parseSoftwareupdateList(test.input)
|
||||
if test.want != got {
|
||||
t.Fatalf("got %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePacmanVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
out string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "valid version",
|
||||
out: `
|
||||
:: Synchronizing package databases...
|
||||
endeavouros is up to date
|
||||
core is up to date
|
||||
extra is up to date
|
||||
multilib is up to date
|
||||
Repository : extra
|
||||
Name : tailscale
|
||||
Version : 1.44.2-1
|
||||
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
|
||||
Architecture : x86_64
|
||||
URL : https://tailscale.com
|
||||
Licenses : MIT
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : glibc
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 7.98 MiB
|
||||
Installed Size : 32.47 MiB
|
||||
Packager : Christian Heusel <gromit@archlinux.org>
|
||||
Build Date : Tue 18 Jul 2023 12:28:37 PM PDT
|
||||
Validated By : MD5 Sum SHA-256 Sum Signature
|
||||
`,
|
||||
want: "1.44.2",
|
||||
},
|
||||
{
|
||||
desc: "version without Arch patch number",
|
||||
out: `
|
||||
... snip ...
|
||||
Name : tailscale
|
||||
Version : 1.44.2
|
||||
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
|
||||
... snip ...
|
||||
`,
|
||||
want: "1.44.2",
|
||||
},
|
||||
{
|
||||
desc: "missing version",
|
||||
out: `
|
||||
... snip ...
|
||||
Name : tailscale
|
||||
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
|
||||
... snip ...
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty version",
|
||||
out: `
|
||||
... snip ...
|
||||
Name : tailscale
|
||||
Version :
|
||||
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
|
||||
... snip ...
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty input",
|
||||
out: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "sneaky version in description",
|
||||
out: `
|
||||
... snip ...
|
||||
Name : tailscale
|
||||
Description : A mesh VPN that makes it easy to connect your devices, wherever they are. Version : 1.2.3
|
||||
Version : 1.44.2
|
||||
... snip ...
|
||||
`,
|
||||
want: "1.44.2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := parsePacmanVersion([]byte(tt.out))
|
||||
if err == nil && tt.wantErr {
|
||||
t.Fatalf("got nil error and version %q, want non-nil error", got)
|
||||
}
|
||||
if err != nil && !tt.wantErr {
|
||||
t.Fatalf("got error: %q, want nil", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got version: %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateYUMRepoTrack(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
before string
|
||||
track string
|
||||
after string
|
||||
rewrote bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "same track",
|
||||
before: `
|
||||
[tailscale-stable]
|
||||
name=Tailscale stable
|
||||
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||
enabled=1
|
||||
type=rpm
|
||||
repo_gpgcheck=1
|
||||
gpgcheck=0
|
||||
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||
`,
|
||||
track: "stable",
|
||||
after: `
|
||||
[tailscale-stable]
|
||||
name=Tailscale stable
|
||||
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||
enabled=1
|
||||
type=rpm
|
||||
repo_gpgcheck=1
|
||||
gpgcheck=0
|
||||
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "change track",
|
||||
before: `
|
||||
[tailscale-stable]
|
||||
name=Tailscale stable
|
||||
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
||||
enabled=1
|
||||
type=rpm
|
||||
repo_gpgcheck=1
|
||||
gpgcheck=0
|
||||
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
||||
`,
|
||||
track: "unstable",
|
||||
after: `
|
||||
[tailscale-unstable]
|
||||
name=Tailscale unstable
|
||||
baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
|
||||
enabled=1
|
||||
type=rpm
|
||||
repo_gpgcheck=1
|
||||
gpgcheck=0
|
||||
gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
|
||||
`,
|
||||
rewrote: true,
|
||||
},
|
||||
{
|
||||
desc: "non-tailscale repo file",
|
||||
before: `
|
||||
[fedora]
|
||||
name=Fedora $releasever - $basearch
|
||||
#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
|
||||
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
|
||||
enabled=1
|
||||
countme=1
|
||||
metadata_expire=7d
|
||||
repo_gpgcheck=0
|
||||
type=rpm
|
||||
gpgcheck=1
|
||||
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
|
||||
skip_if_unavailable=False
|
||||
`,
|
||||
track: "stable",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "tailscale.repo")
|
||||
if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rewrote, err := updateYUMRepoTrack(path, tt.track)
|
||||
if err == nil && tt.wantErr {
|
||||
t.Fatal("got nil error, want non-nil")
|
||||
}
|
||||
if err != nil && !tt.wantErr {
|
||||
t.Fatalf("got error %q, want nil", err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if rewrote != tt.rewrote {
|
||||
t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
|
||||
}
|
||||
|
||||
after, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(after) != tt.after {
|
||||
t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAlpinePackageVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
out string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "valid version",
|
||||
out: `
|
||||
tailscale-1.44.2-r0 description:
|
||||
The easiest, most secure way to use WireGuard and 2FA
|
||||
|
||||
tailscale-1.44.2-r0 webpage:
|
||||
https://tailscale.com/
|
||||
|
||||
tailscale-1.44.2-r0 installed size:
|
||||
32 MiB
|
||||
`,
|
||||
want: "1.44.2",
|
||||
},
|
||||
{
|
||||
desc: "wrong package output",
|
||||
out: `
|
||||
busybox-1.36.1-r0 description:
|
||||
Size optimized toolbox of many common UNIX utilities
|
||||
|
||||
busybox-1.36.1-r0 webpage:
|
||||
https://busybox.net/
|
||||
|
||||
busybox-1.36.1-r0 installed size:
|
||||
924 KiB
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "missing version",
|
||||
out: `
|
||||
tailscale description:
|
||||
The easiest, most secure way to use WireGuard and 2FA
|
||||
|
||||
tailscale webpage:
|
||||
https://tailscale.com/
|
||||
|
||||
tailscale installed size:
|
||||
32 MiB
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty output",
|
||||
out: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := parseAlpinePackageVersion([]byte(tt.out))
|
||||
if err == nil && tt.wantErr {
|
||||
t.Fatalf("got nil error and version %q, want non-nil error", got)
|
||||
}
|
||||
if err != nil && !tt.wantErr {
|
||||
t.Fatalf("got error: %q, want nil", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got version: %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,16 @@ var versionCmd = &ffcli.Command{
|
||||
fs := newFlagSet("version")
|
||||
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
|
||||
fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format")
|
||||
fs.BoolVar(&versionArgs.upstream, "upstream", false, "fetch and print the latest upstream release version from pkgs.tailscale.com")
|
||||
return fs
|
||||
})(),
|
||||
Exec: runVersion,
|
||||
}
|
||||
|
||||
var versionArgs struct {
|
||||
daemon bool // also check local node's daemon version
|
||||
json bool
|
||||
daemon bool // also check local node's daemon version
|
||||
json bool
|
||||
upstream bool
|
||||
}
|
||||
|
||||
func runVersion(ctx context.Context, args []string) error {
|
||||
@@ -47,21 +49,46 @@ func runVersion(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
var upstreamVer string
|
||||
if versionArgs.upstream {
|
||||
track := "stable"
|
||||
if version.IsUnstableBuild() {
|
||||
track = "unstable"
|
||||
}
|
||||
upstreamVer, err = latestTailscaleVersion(track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if versionArgs.json {
|
||||
m := version.GetMeta()
|
||||
if st != nil {
|
||||
m.DaemonLong = st.Version
|
||||
}
|
||||
out := struct {
|
||||
version.Meta
|
||||
Upstream string `json:"upstream,omitempty"`
|
||||
}{
|
||||
Meta: m,
|
||||
Upstream: upstreamVer,
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
return e.Encode(m)
|
||||
return e.Encode(out)
|
||||
}
|
||||
|
||||
if st == nil {
|
||||
outln(version.String())
|
||||
if versionArgs.upstream {
|
||||
printf(" upstream: %s\n", upstreamVer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
printf("Client: %s\n", version.String())
|
||||
printf("Daemon: %s\n", st.Version)
|
||||
if versionArgs.upstream {
|
||||
printf("Upstream: %s\n", upstreamVer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/webui"
|
||||
)
|
||||
|
||||
//go:embed web.html
|
||||
@@ -61,6 +63,8 @@ type tmplData struct {
|
||||
TUNMode bool
|
||||
IsSynology bool
|
||||
DSMVersion int // 6 or 7, if IsSynology=true
|
||||
IsUnraid bool
|
||||
UnraidToken string
|
||||
IPNVersion string
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
webf := newFlagSet("web")
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
webf.BoolVar(&webArgs.dev, "dev", false, "run in dev mode")
|
||||
return webf
|
||||
})(),
|
||||
Exec: runWeb,
|
||||
@@ -96,6 +101,7 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
var webArgs struct {
|
||||
listen string
|
||||
cgi bool
|
||||
dev bool
|
||||
}
|
||||
|
||||
func tlsConfigFromEnvironment() *tls.Config {
|
||||
@@ -126,8 +132,18 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
handler := webHandler
|
||||
if true {
|
||||
newServer := &webui.Server{
|
||||
DevMode: webArgs.dev,
|
||||
}
|
||||
cleanup := webui.RunJSDevServer()
|
||||
defer cleanup()
|
||||
handler = newServer.Handle
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
|
||||
if err := cgi.Serve(http.HandlerFunc(handler)); err != nil {
|
||||
log.Printf("tailscale.cgi: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -139,24 +155,21 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
server := &http.Server{
|
||||
Addr: webArgs.listen,
|
||||
TLSConfig: tlsConfig,
|
||||
Handler: http.HandlerFunc(webHandler),
|
||||
Handler: http.HandlerFunc(handler),
|
||||
}
|
||||
|
||||
log.Printf("web server running on: https://%s", server.Addr)
|
||||
return server.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(handler))
|
||||
}
|
||||
}
|
||||
|
||||
// urlOfListenAddr parses a given listen address into a formatted URL
|
||||
func urlOfListenAddr(addr string) string {
|
||||
host, port, _ := net.SplitHostPort(addr)
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
|
||||
return fmt.Sprintf("http://%s", net.JoinHostPort(cmpx.Or(host, "127.0.0.1"), port))
|
||||
}
|
||||
|
||||
// authorize returns the name of the user accessing the web UI after verifying
|
||||
@@ -228,33 +241,48 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||
return "", nil, fmt.Errorf("not authenticated by any mechanism")
|
||||
}
|
||||
|
||||
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
|
||||
// running based on the request URL. This is necessary because QNAP has so
|
||||
// many options, see https://github.com/tailscale/tailscale/issues/7108
|
||||
// and https://github.com/tailscale/tailscale/issues/6903
|
||||
func qnapAuthnURL(requestUrl string, query url.Values) string {
|
||||
in, err := url.Parse(requestUrl)
|
||||
scheme := ""
|
||||
host := ""
|
||||
if err != nil || in.Scheme == "" {
|
||||
log.Printf("Cannot parse QNAP login URL %v", err)
|
||||
|
||||
// try localhost and hope for the best
|
||||
scheme = "http"
|
||||
host = "localhost"
|
||||
} else {
|
||||
scheme = in.Scheme
|
||||
host = in.Host
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"qtoken": []string{token},
|
||||
"user": []string{user},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
|
||||
}
|
||||
|
||||
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"sid": []string{sid},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
|
||||
}
|
||||
|
||||
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
@@ -426,6 +454,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
TUNMode: st.TUN,
|
||||
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
|
||||
DSMVersion: distro.DSMVersion(),
|
||||
IsUnraid: distro.Get() == distro.Unraid,
|
||||
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
||||
IPNVersion: versionShort,
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
</svg>
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
{{ with .Profile }}
|
||||
<div class="text-right w-full leading-4">
|
||||
<h4 class="truncate leading-normal">{{.}}</h4>
|
||||
<h4 class="truncate leading-normal">{{.LoginName}}</h4>
|
||||
<div class="text-xs text-gray-500 text-right">
|
||||
<a href="#" class="hover:text-gray-700 js-loginButton">Switch account</a> | <a href="#"
|
||||
class="hover:text-gray-700 js-loginButton">Reauthenticate</a> | <a href="#"
|
||||
@@ -116,10 +116,12 @@
|
||||
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
|
||||
</footer>
|
||||
<script>(function () {
|
||||
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||
const advertiseExitNode = {{ .AdvertiseExitNode }};
|
||||
const isUnraid = {{ .IsUnraid }};
|
||||
const unraidCsrfToken = "{{ .UnraidToken }}";
|
||||
let fetchingUrl = false;
|
||||
var data = {
|
||||
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
|
||||
AdvertiseRoutes: "{{ .AdvertiseRoutes }}",
|
||||
AdvertiseExitNode: advertiseExitNode,
|
||||
Reauthenticate: false,
|
||||
ForceLogout: false
|
||||
@@ -141,15 +143,27 @@ function postData(e) {
|
||||
}
|
||||
const nextUrl = new URL(window.location);
|
||||
nextUrl.search = nextParams.toString()
|
||||
const url = nextUrl.toString();
|
||||
|
||||
let body = JSON.stringify(data);
|
||||
let contentType = "application/json";
|
||||
|
||||
if (isUnraid) {
|
||||
const params = new URLSearchParams();
|
||||
params.append("csrf_token", unraidCsrfToken);
|
||||
params.append("ts_data", JSON.stringify(data));
|
||||
|
||||
body = params.toString();
|
||||
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
|
||||
}
|
||||
|
||||
const url = nextUrl.toString();
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": contentType,
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: body
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
@@ -158,7 +172,11 @@ function postData(e) {
|
||||
}
|
||||
const url = res["url"];
|
||||
if (url) {
|
||||
document.location.href = url;
|
||||
if(isUnraid) {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
document.location.href = url;
|
||||
}
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUrlOfListenAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -34,9 +37,64 @@ func TestUrlOfListenAddr(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := urlOfListenAddr(tt.in)
|
||||
if url != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, url)
|
||||
u := urlOfListenAddr(tt.in)
|
||||
if u != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, u)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQnapAuthnURL(t *testing.T) {
|
||||
query := url.Values{
|
||||
"qtoken": []string{"token"},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "localhost http",
|
||||
in: "http://localhost:8088/",
|
||||
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "localhost https",
|
||||
in: "https://localhost:5000/",
|
||||
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "IP http",
|
||||
in: "http://10.1.20.4:80/",
|
||||
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "IP6 https",
|
||||
in: "https://[ff7d:0:1:2::1]/",
|
||||
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "hostname https",
|
||||
in: "https://qnap.example.com/",
|
||||
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "invalid URL",
|
||||
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
|
||||
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "err != nil",
|
||||
in: "http://192.168.0.%31/",
|
||||
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u := qnapAuthnURL(tt.in, query)
|
||||
if u != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, u)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,17 +3,28 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil/authenticode
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
@@ -21,7 +32,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
github.com/miekg/dns from tailscale.com/net/dns/recursive
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
@@ -34,11 +47,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
@@ -52,7 +68,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
@@ -66,6 +82,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
@@ -74,12 +91,15 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netmon from tailscale.com/net/sockstats+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
@@ -90,8 +110,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
@@ -110,18 +131,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
|
||||
@@ -139,7 +164,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices+
|
||||
golang.org/x/exp/maps from tailscale.com/types/views+
|
||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
@@ -149,9 +175,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.org/x/net/icmp+
|
||||
golang.org/x/net/ipv6 from golang.org/x/net/icmp
|
||||
golang.org/x/net/ipv6 from golang.org/x/net/icmp+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
|
||||
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
@@ -168,7 +197,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from image/png
|
||||
compress/zlib from image/png+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -193,10 +222,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
database/sql/driver from github.com/google/uuid
|
||||
W debug/dwarf from debug/pe
|
||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka
|
||||
encoding/base32 from tailscale.com/tka+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
@@ -220,6 +251,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
|
||||
@@ -6,4 +6,3 @@ package main
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
|
||||
Binary file not shown.
@@ -14,25 +14,19 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
@@ -48,7 +42,7 @@ var debugModeFunc = debugMode // so it can be addressable
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
@@ -82,7 +76,7 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf)
|
||||
mon, err := netmon.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -90,10 +84,10 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
log.Printf("Network monitor fired; no change")
|
||||
return
|
||||
}
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
log.Printf("Network monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
if loop {
|
||||
@@ -199,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c1.Close()
|
||||
@@ -229,95 +223,5 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "pcp":
|
||||
portmapper.DisablePMP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "upnp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisablePMP = true
|
||||
default:
|
||||
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
|
||||
}
|
||||
|
||||
done := make(chan bool, 1)
|
||||
|
||||
var c *portmapper.Client
|
||||
logf := log.Printf
|
||||
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("cb: mapping: %v", ext)
|
||||
select {
|
||||
case done <- true:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
logf("cb: no mapping")
|
||||
})
|
||||
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
|
||||
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
|
||||
i := strings.Index(v, "/")
|
||||
gw = netip.MustParseAddr(v[:i])
|
||||
self = netip.MustParseAddr(v[i+1:])
|
||||
return gw, self, true
|
||||
}
|
||||
return linkMon.GatewayAndSelfIP()
|
||||
}
|
||||
|
||||
c.SetGatewayLookupFunc(gatewayAndSelfIP)
|
||||
|
||||
gw, selfIP, ok := gatewayAndSelfIP()
|
||||
if !ok {
|
||||
logf("no gateway or self IP; %v", linkMon.InterfaceState())
|
||||
return nil
|
||||
}
|
||||
logf("gw=%v; self=%v", gw, selfIP)
|
||||
|
||||
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uc.Close()
|
||||
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
|
||||
|
||||
res, err := c.Probe(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Probe: %v", err)
|
||||
}
|
||||
logf("Probe: %+v", res)
|
||||
|
||||
if !res.PCP && !res.PMP && !res.UPnP {
|
||||
logf("no portmapping services available")
|
||||
return nil
|
||||
}
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("mapping: %v", ext)
|
||||
} else {
|
||||
logf("no mapping")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
@@ -12,7 +14,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
@@ -38,6 +40,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||
L github.com/aws/aws-sdk-go-v2/internal/shareddefaults from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
|
||||
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||
@@ -48,16 +51,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssooidc
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc/types from github.com/aws/aws-sdk-go-v2/service/ssooidc
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
|
||||
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
|
||||
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws
|
||||
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/context from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
|
||||
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
@@ -69,16 +75,24 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
|
||||
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
@@ -93,7 +107,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd+
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
@@ -102,15 +116,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
github.com/miekg/dns from tailscale.com/net/dns/recursive
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
L github.com/pierrec/lz4/v4 from github.com/u-root/uio/uio
|
||||
L github.com/pierrec/lz4/v4/internal/lz4block from github.com/pierrec/lz4/v4+
|
||||
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
|
||||
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
|
||||
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
|
||||
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
|
||||
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
@@ -154,6 +176,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
@@ -200,6 +223,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
@@ -212,21 +236,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
|
||||
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
|
||||
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
@@ -237,21 +263,25 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun/table from tailscale.com/net/tstun
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
@@ -260,13 +290,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock+
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
@@ -288,6 +320,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
@@ -296,17 +329,24 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
@@ -314,7 +354,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/netlog from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
@@ -323,7 +362,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
@@ -340,13 +378,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices+
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine+
|
||||
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http+
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
@@ -374,6 +412,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from golang.org/x/net/http2+
|
||||
W compress/zlib from debug/pe
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -398,10 +437,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
W debug/dwarf from debug/pe
|
||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||
embed from tailscale.com+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka
|
||||
encoding/base32 from tailscale.com/tka+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
@@ -410,10 +451,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/control/controlclient+
|
||||
flag from net/http/httptest+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
@@ -422,6 +463,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
|
||||
@@ -6,4 +6,3 @@ package main
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
|
||||
Binary file not shown.
@@ -18,7 +18,7 @@ import (
|
||||
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
|
||||
dg := distro.Get()
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS, distro.QNAP:
|
||||
case distro.Synology, distro.TrueNAS, distro.QNAP, distro.Unraid:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
@@ -42,6 +42,8 @@ func findTaildropDir(dg distro.Distro) (string, error) {
|
||||
return findTrueNASTaildropDir(name)
|
||||
case distro.QNAP:
|
||||
return findQnapTaildropDir(name)
|
||||
case distro.Unraid:
|
||||
return findUnraidTaildropDir(name)
|
||||
}
|
||||
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
|
||||
}
|
||||
@@ -103,3 +105,25 @@ func findQnapTaildropDir(name string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findUnraidTaildropDir looks for a directory linked at
|
||||
// /var/lib/tailscale/Taildrop. This is a symlink to the
|
||||
// path specified by the user in the Unraid Web UI
|
||||
func findUnraidTaildropDir(name string) (string, error) {
|
||||
dir := fmt.Sprintf("/var/lib/tailscale/%s", name)
|
||||
_, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink %q not found", name)
|
||||
}
|
||||
|
||||
fullpath, err := filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink %q to shared folder not valid", name)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(fullpath)
|
||||
if err == nil && fi.IsDir() {
|
||||
return dir, nil // return the symlink
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
@@ -39,25 +39,28 @@ import (
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -327,7 +330,19 @@ var logPol *logpolicy.Policy
|
||||
var debugMux *http.ServeMux
|
||||
|
||||
func run() error {
|
||||
pol := logpolicy.New(logtail.CollectionNode)
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
sys := new(tsd.System)
|
||||
|
||||
netMon, err := netmon.New(func(format string, args ...any) {
|
||||
logf(format, args...)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("netmon.New: %w", err)
|
||||
}
|
||||
sys.Set(netMon)
|
||||
|
||||
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
logPol = pol
|
||||
defer func() {
|
||||
@@ -351,7 +366,6 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if envknob.Bool("TS_DEBUG_MEMORY") {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
@@ -377,11 +391,10 @@ func run() error {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
logid := pol.PublicID.String()
|
||||
return startIPNServer(context.Background(), logf, logid)
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
|
||||
ln, err := safesocket.Listen(args.socketpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
@@ -407,7 +420,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
srv := ipnserver.New(logf, logid)
|
||||
srv := ipnserver.New(logf, logID, sys.NetMon.Get())
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
@@ -425,7 +438,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
return
|
||||
}
|
||||
}
|
||||
lb, err := getLocalBackend(ctx, logf, logid)
|
||||
lb, err := getLocalBackend(ctx, logf, logID, sys)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
srv.SetLocalBackend(lb)
|
||||
@@ -449,35 +462,28 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
if logPol != nil {
|
||||
logPol.Logtail.SetLinkMonitor(linkMon)
|
||||
logPol.Logtail.SetNetMon(sys.NetMon.Get())
|
||||
}
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
sys.Set(dialer)
|
||||
|
||||
onlyNetstack, err := createEngine(logf, sys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
}
|
||||
if debugMux != nil {
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if _, mc, _, ok := ig.GetInternals(); ok {
|
||||
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
|
||||
}
|
||||
if ms, ok := sys.MagicSock.GetOK(); ok {
|
||||
debugMux.HandleFunc("/debug/magicsock", ms.ServeHTTPDebug)
|
||||
}
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
ns, err := newNetstack(logf, sys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
@@ -485,6 +491,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
|
||||
|
||||
if onlyNetstack {
|
||||
e := sys.Engine.Get()
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
@@ -494,11 +501,13 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
}
|
||||
}
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
var addrs []string
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
go func() {
|
||||
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
||||
}()
|
||||
addrs = append(addrs, httpProxyListener.Addr().String())
|
||||
}
|
||||
if socksListener != nil {
|
||||
ss := &socks5.Server{
|
||||
@@ -508,19 +517,20 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
addrs = append(addrs, socksListener.Addr().String())
|
||||
}
|
||||
tshttpproxy.SetSelfProxy(addrs...)
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
opts := ipnServerOpts()
|
||||
|
||||
store, err := store.New(logf, statePathOrDefault())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store.New: %w", err)
|
||||
}
|
||||
sys.Set(store)
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, e, opts.LoginFlags)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
}
|
||||
@@ -529,7 +539,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
lb.SetLogFlusher(logPol.Logtail.StartFlush)
|
||||
}
|
||||
if root := lb.TailscaleVarRoot(); root != "" {
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
||||
}
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
@@ -546,21 +556,21 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
//
|
||||
// onlyNetstack is true if the user has explicitly requested that we use netstack
|
||||
// for all networking.
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
func createEngine(logf logger.Logf, sys *tsd.System) (onlyNetstack bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
return false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, onlyNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
onlyNetstack, err = tryEngine(logf, sys, name)
|
||||
if err == nil {
|
||||
return e, onlyNetstack, nil
|
||||
return onlyNetstack, nil
|
||||
}
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil, false, multierr.New(errs...)
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
|
||||
// handleSubnetsInNetstack reports whether netstack should handle subnet routers
|
||||
@@ -585,21 +595,23 @@ func handleSubnetsInNetstack() bool {
|
||||
|
||||
var tstunNew = tstun.New
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
}
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
|
||||
netns.SetEnabled(!onlyNetstack)
|
||||
|
||||
if args.birdSocketPath != "" && createBIRDClient != nil {
|
||||
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
|
||||
return false, fmt.Errorf("createBIRDClient: %w", err)
|
||||
}
|
||||
}
|
||||
if onlyNetstack {
|
||||
@@ -612,44 +624,55 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
// TODO(bradfitz): add a Synology-specific DNS manager.
|
||||
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dev, devName, err := tstunNew(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name, err)
|
||||
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
return false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
conf.IsTAP = true
|
||||
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
return e, false, err
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sys.Set(e)
|
||||
return false, err
|
||||
}
|
||||
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
r, err := router.New(logf, dev, sys.NetMon.Get())
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, fmt.Errorf("creating router: %w", err)
|
||||
return false, fmt.Errorf("creating router: %w", err)
|
||||
}
|
||||
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
r.Close()
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if handleSubnetsInNetstack() {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
netstackSubnetRouter = true
|
||||
}
|
||||
sys.Set(conf.Router)
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, onlyNetstack, err
|
||||
return onlyNetstack, err
|
||||
}
|
||||
return e, onlyNetstack, nil
|
||||
e = wgengine.NewWatchdog(e)
|
||||
sys.Set(e)
|
||||
sys.NetstackRouter.Set(netstackSubnetRouter)
|
||||
|
||||
return onlyNetstack, nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
@@ -665,7 +688,7 @@ func newDebugMux() *http.ServeMux {
|
||||
|
||||
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
tsweb.VarzHandler(w, r)
|
||||
varz.Handler(w, r)
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
@@ -679,12 +702,8 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
tunDev, magicConn, dns, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
return netstack.Create(logf, tunDev, e, magicConn, dialer, dns)
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
return netstack.Create(logf, sys.Tun.Get(), sys.Engine.Get(), sys.MagicSock.Get(), sys.Dialer.Get(), sys.DNSManager.Get())
|
||||
}
|
||||
|
||||
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
|
||||
|
||||
@@ -3,10 +3,32 @@
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
// This test does nothing on purpose, so we can run
|
||||
// GODEBUG=memprofilerate=1 go test -v -run=Nothing -memprofile=prof.mem
|
||||
// without any errors about no matching tests.
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
GOOS: "darwin",
|
||||
GOARCH: "arm64",
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
|
||||
},
|
||||
}.Check(t)
|
||||
|
||||
deptest.DepChecker{
|
||||
GOOS: "linux",
|
||||
GOARCH: "arm64",
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,12 @@ import (
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/osdiag"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
@@ -123,6 +127,10 @@ var syslogf logger.Logf = logger.Discard
|
||||
// At this point we're still the parent process that
|
||||
// Windows started.
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
go func() {
|
||||
osdiag.LogSupportInfo(logger.WithPrefix(log.Printf, "Support Info: "), osdiag.LogSupportInfoReasonStartup)
|
||||
}()
|
||||
|
||||
if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {
|
||||
syslog, err := eventlog.Open(serviceName)
|
||||
if err == nil {
|
||||
@@ -262,13 +270,13 @@ func beWindowsSubprocess() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
logid := os.Args[2]
|
||||
logID := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file loggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long(), os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
log.Printf("subproc mode: logid=%v", logID)
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
@@ -290,7 +298,15 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(ctx, log.Printf, logid)
|
||||
sys := new(tsd.System)
|
||||
netMon, err := netmon.New(log.Printf)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create netMon: %v", err)
|
||||
}
|
||||
sys.Set(netMon)
|
||||
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,16 +7,20 @@
|
||||
package flakytest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// InTestWrapper returns whether or not this binary is running under our test
|
||||
// wrapper.
|
||||
func InTestWrapper() bool {
|
||||
return os.Getenv("TS_IN_TESTWRAPPER") != ""
|
||||
}
|
||||
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
|
||||
// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
|
||||
// and retry them.
|
||||
const FlakyTestLogMessage = "flakytest: this is a known flaky test"
|
||||
|
||||
// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
|
||||
// when a flaky test is retried. It contains the attempt number, starting at 1.
|
||||
const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
|
||||
|
||||
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
|
||||
|
||||
@@ -30,16 +34,6 @@ func Mark(t testing.TB, issue string) {
|
||||
t.Fatalf("bad issue format: %q", issue)
|
||||
}
|
||||
|
||||
if !InTestWrapper() {
|
||||
return
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
t.Logf("flakytest: signaling test wrapper to retry test")
|
||||
|
||||
// Signal to test wrapper that we should restart.
|
||||
os.Exit(123)
|
||||
}
|
||||
})
|
||||
fmt.Fprintln(os.Stderr, FlakyTestLogMessage) // sentinel value for testwrapper
|
||||
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package flakytest
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIssueFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
@@ -24,3 +27,17 @@ func TestIssueFormat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlakeRun is a test that fails when run in the testwrapper
|
||||
// for the first time, but succeeds on the second run.
|
||||
// It's used to test whether the testwrapper retries flaky tests.
|
||||
func TestFlakeRun(t *testing.T) {
|
||||
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
|
||||
e := os.Getenv(FlakeAttemptEnv)
|
||||
if e == "" {
|
||||
t.Skip("not running in testwrapper")
|
||||
}
|
||||
if e == "1" {
|
||||
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,288 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
|
||||
// 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
|
||||
// themselves as flaky and be retried on failure.
|
||||
// testwrapper is a wrapper for retrying flaky tests. It is an alternative to
|
||||
// `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It
|
||||
// takes different arguments than go test and requires the first positional
|
||||
// argument to be the pattern to test.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
)
|
||||
|
||||
const (
|
||||
retryStatus = 123
|
||||
maxIterations = 3
|
||||
)
|
||||
const maxAttempts = 3
|
||||
|
||||
type testAttempt struct {
|
||||
name testName
|
||||
outcome string // "pass", "fail", "skip"
|
||||
logs bytes.Buffer
|
||||
isMarkedFlaky bool // set if the test is marked as flaky
|
||||
|
||||
pkgFinished bool
|
||||
}
|
||||
|
||||
type testName struct {
|
||||
pkg string // "tailscale.com/types/key"
|
||||
name string // "TestFoo"
|
||||
}
|
||||
|
||||
type packageTests struct {
|
||||
// pattern is the package pattern to run.
|
||||
// Must be a single pattern, not a list of patterns.
|
||||
pattern string // "./...", "./types/key"
|
||||
// tests is a list of tests to run. If empty, all tests in the package are
|
||||
// run.
|
||||
tests []string // ["TestFoo", "TestBar"]
|
||||
}
|
||||
|
||||
type goTestOutput struct {
|
||||
Time time.Time
|
||||
Action string
|
||||
Package string
|
||||
Test string
|
||||
Output string
|
||||
}
|
||||
|
||||
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
|
||||
// runTests runs the tests in pt and sends the results on ch. It sends a
|
||||
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
|
||||
// set to true.
|
||||
// It calls close(ch) when it's done.
|
||||
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
|
||||
defer close(ch)
|
||||
args := []string{"test", "-json", pt.pattern}
|
||||
args = append(args, otherArgs...)
|
||||
if len(pt.tests) > 0 {
|
||||
runArg := strings.Join(pt.tests, "|")
|
||||
args = append(args, "-run", runArg)
|
||||
}
|
||||
if debug {
|
||||
fmt.Println("running", strings.Join(args, " "))
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
r, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("error creating stdout pipe: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt))
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("error starting test: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
cmd.Wait()
|
||||
}()
|
||||
|
||||
jd := json.NewDecoder(r)
|
||||
resultMap := make(map[testName]*testAttempt)
|
||||
for {
|
||||
var goOutput goTestOutput
|
||||
if err := jd.Decode(&goOutput); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
|
||||
break
|
||||
}
|
||||
|
||||
// `go test -json` outputs invalid JSON when a build fails.
|
||||
// In that case, discard the the output and start reading again.
|
||||
// The build error will be printed to stderr.
|
||||
// See: https://github.com/golang/go/issues/35169
|
||||
if _, ok := err.(*json.SyntaxError); ok {
|
||||
jd = json.NewDecoder(r)
|
||||
continue
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if goOutput.Test == "" {
|
||||
switch goOutput.Action {
|
||||
case "fail", "pass", "skip":
|
||||
ch <- &testAttempt{
|
||||
name: testName{
|
||||
pkg: goOutput.Package,
|
||||
},
|
||||
outcome: goOutput.Action,
|
||||
pkgFinished: true,
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
name := testName{
|
||||
pkg: goOutput.Package,
|
||||
name: goOutput.Test,
|
||||
}
|
||||
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
|
||||
name.name = test
|
||||
if goOutput.Action == "output" {
|
||||
resultMap[name].logs.WriteString(goOutput.Output)
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch goOutput.Action {
|
||||
case "start":
|
||||
// ignore
|
||||
case "run":
|
||||
resultMap[name] = &testAttempt{
|
||||
name: name,
|
||||
}
|
||||
case "skip", "pass", "fail":
|
||||
resultMap[name].outcome = goOutput.Action
|
||||
ch <- resultMap[name]
|
||||
case "output":
|
||||
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
|
||||
resultMap[name].isMarkedFlaky = true
|
||||
} else {
|
||||
resultMap[name].logs.WriteString(goOutput.Output)
|
||||
}
|
||||
}
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
debug := os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
|
||||
log.SetPrefix("testwrapper: ")
|
||||
if !debug {
|
||||
log.SetFlags(0)
|
||||
// We only need to parse the -v flag to figure out whether to print the logs
|
||||
// for a test. We don't need to parse any other flags, so we just use the
|
||||
// flag package to parse the -v flag and then pass the rest of the args
|
||||
// through to 'go test'.
|
||||
// We run `go test -json` which returns the same information as `go test -v`,
|
||||
// but in a machine-readable format. So this flag is only for testwrapper's
|
||||
// output.
|
||||
v := flag.Bool("v", false, "verbose")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
|
||||
fmt.Println()
|
||||
fmt.Println("testwrapper-flags:")
|
||||
flag.CommandLine.PrintDefaults()
|
||||
fmt.Println()
|
||||
fmt.Println("examples:")
|
||||
fmt.Println("\ttestwrapper -v ./... -count=1")
|
||||
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
|
||||
fmt.Println()
|
||||
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
|
||||
fmt.Println("no pattern specified")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
} else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
|
||||
fmt.Println("expected single pattern")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
pattern, otherArgs := args[0], args[1:]
|
||||
|
||||
type nextRun struct {
|
||||
tests []*packageTests
|
||||
attempt int
|
||||
}
|
||||
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
if i > 1 {
|
||||
log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
toRun := []*nextRun{
|
||||
{
|
||||
tests: []*packageTests{{pattern: pattern}},
|
||||
attempt: 1,
|
||||
},
|
||||
}
|
||||
printPkgOutcome := func(pkg, outcome string, attempt int) {
|
||||
if outcome == "skip" {
|
||||
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
|
||||
return
|
||||
}
|
||||
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
if debug {
|
||||
log.Printf("error isn't an ExitError")
|
||||
}
|
||||
os.Exit(1)
|
||||
if outcome == "pass" {
|
||||
outcome = "ok"
|
||||
}
|
||||
|
||||
if code := exitErr.ExitCode(); code != retryStatus {
|
||||
if debug {
|
||||
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
|
||||
}
|
||||
os.Exit(code)
|
||||
if outcome == "fail" {
|
||||
outcome = "FAIL"
|
||||
}
|
||||
if attempt > 1 {
|
||||
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\t%s\n", outcome, pkg)
|
||||
}
|
||||
|
||||
log.Printf("test did not pass in %d iterations", maxIterations)
|
||||
os.Exit(1)
|
||||
for len(toRun) > 0 {
|
||||
var thisRun *nextRun
|
||||
thisRun, toRun = toRun[0], toRun[1:]
|
||||
|
||||
if thisRun.attempt >= maxAttempts {
|
||||
fmt.Println("max attempts reached")
|
||||
os.Exit(1)
|
||||
}
|
||||
if thisRun.attempt > 1 {
|
||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
|
||||
}
|
||||
|
||||
failed := false
|
||||
toRetry := make(map[string][]string) // pkg -> tests to retry
|
||||
for _, pt := range thisRun.tests {
|
||||
ch := make(chan *testAttempt)
|
||||
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
|
||||
for tr := range ch {
|
||||
if tr.pkgFinished {
|
||||
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
|
||||
continue
|
||||
}
|
||||
if *v || tr.outcome == "fail" {
|
||||
io.Copy(os.Stdout, &tr.logs)
|
||||
}
|
||||
if tr.outcome != "fail" {
|
||||
continue
|
||||
}
|
||||
if tr.isMarkedFlaky {
|
||||
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(toRetry) == 0 {
|
||||
continue
|
||||
}
|
||||
pkgs := maps.Keys(toRetry)
|
||||
sort.Strings(pkgs)
|
||||
nextRun := &nextRun{
|
||||
attempt: thisRun.attempt + 1,
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
tests := toRetry[pkg]
|
||||
sort.Strings(tests)
|
||||
nextRun.tests = append(nextRun.tests, &packageTests{
|
||||
pattern: pkg,
|
||||
tests: tests,
|
||||
})
|
||||
}
|
||||
toRun = append(toRun, nextRun)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
|
||||
}
|
||||
|
||||
func findRepoRoot() (string, error) {
|
||||
if *rootDir != "" {
|
||||
return *rootDir, nil
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -38,13 +38,31 @@ class App extends Component<{}, AppState> {
|
||||
if (ipnState === "NeedsMachineAuth") {
|
||||
machineAuthInstructions = (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
An administrator needs to authorize this device.
|
||||
An administrator needs to approve this device.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const lockedOut = netMap?.lockedOut
|
||||
let lockedOutInstructions
|
||||
if (lockedOut) {
|
||||
lockedOutInstructions = (
|
||||
<div class="container mx-auto px-4 text-center space-y-4">
|
||||
<p>This instance of Tailscale Connect needs to be signed, due to
|
||||
{" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
|
||||
being enabled on this domain.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Run the following command on a device with a trusted tailnet lock key:
|
||||
<pre>tailscale lock sign {netMap.self.nodeKey}</pre>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let ssh
|
||||
if (ipn && ipnState === "Running" && netMap) {
|
||||
if (ipn && ipnState === "Running" && netMap && !lockedOut) {
|
||||
ssh = <SSH netMap={netMap} ipn={ipn} />
|
||||
}
|
||||
|
||||
@@ -55,6 +73,7 @@ class App extends Component<{}, AppState> {
|
||||
<div class="flex-grow flex flex-col justify-center overflow-hidden">
|
||||
{urlDisplay}
|
||||
{machineAuthInstructions}
|
||||
{lockedOutInstructions}
|
||||
{ssh}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -30,7 +30,7 @@ const STATE_LABELS = {
|
||||
NoState: "Initializing…",
|
||||
InUseOtherUser: "In-use by another user",
|
||||
NeedsLogin: "Needs login",
|
||||
NeedsMachineAuth: "Needs authorization",
|
||||
NeedsMachineAuth: "Needs approval",
|
||||
Stopped: "Stopped",
|
||||
Starting: "Starting…",
|
||||
Running: "Running",
|
||||
|
||||
@@ -60,11 +60,11 @@ function SSHSession({
|
||||
function NoSSHPeers() {
|
||||
return (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
None of your machines have
|
||||
None of your machines have{" "}
|
||||
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
|
||||
Tailscale SSH
|
||||
</a>
|
||||
enabled. Give it a try!
|
||||
{" "}enabled. Give it a try!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
1
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
1
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
@@ -63,6 +63,7 @@ declare global {
|
||||
type IPNNetMap = {
|
||||
self: IPNNetMapSelfNode
|
||||
peers: IPNNetMapPeerNode[]
|
||||
lockedOut: boolean
|
||||
}
|
||||
|
||||
type IPNNetMapNode = {
|
||||
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
|
||||
fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
|
||||
devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
|
||||
rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/words"
|
||||
@@ -46,7 +47,7 @@ import (
|
||||
var ControlURL = ipn.DefaultControlURL
|
||||
|
||||
func main() {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("Usage: newIPN(config)")
|
||||
return nil
|
||||
@@ -96,19 +97,19 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
logf := logtail.Logf
|
||||
|
||||
sys := new(tsd.System)
|
||||
sys.Set(store)
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sys.Set(eng)
|
||||
|
||||
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
@@ -121,10 +122,11 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
sys.NetstackRouter.Set(true)
|
||||
|
||||
logid := lpc.PublicID.String()
|
||||
srv := ipnserver.New(logf, logid)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
|
||||
logid := lpc.PublicID
|
||||
srv := ipnserver.New(logf, logid, nil /* no netMon */)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -146,7 +148,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Fatal(`Usage: run({
|
||||
notifyState(state: int): void,
|
||||
@@ -159,7 +161,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
jsIPN.run(args[0])
|
||||
return nil
|
||||
}),
|
||||
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: login()")
|
||||
return nil
|
||||
@@ -167,7 +169,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
jsIPN.login()
|
||||
return nil
|
||||
}),
|
||||
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: logout()")
|
||||
return nil
|
||||
@@ -175,7 +177,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
jsIPN.logout()
|
||||
return nil
|
||||
}),
|
||||
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 3 {
|
||||
log.Printf("Usage: ssh(hostname, userName, termConfig)")
|
||||
return nil
|
||||
@@ -185,7 +187,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
args[1].String(),
|
||||
args[2])
|
||||
}),
|
||||
"fetch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Printf("Usage: fetch(url)")
|
||||
return nil
|
||||
@@ -272,6 +274,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
|
||||
}
|
||||
}),
|
||||
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
|
||||
}
|
||||
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
|
||||
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
|
||||
@@ -333,10 +336,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
||||
go jsSSHSession.Run()
|
||||
|
||||
return map[string]any{
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return jsSSHSession.Close() != nil
|
||||
}),
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
rows := args[0].Int()
|
||||
cols := args[1].Int()
|
||||
return jsSSHSession.Resize(rows, cols) != nil
|
||||
@@ -425,7 +428,7 @@ func (s *jsSSHSession) Run() {
|
||||
session.Stdout = termWriter{writeFn}
|
||||
session.Stderr = termWriter{writeFn}
|
||||
|
||||
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
input := args[0].String()
|
||||
_, err := stdin.Write([]byte(input))
|
||||
if err != nil {
|
||||
@@ -495,7 +498,7 @@ func (i *jsIPN) fetch(url string) js.Value {
|
||||
return map[string]any{
|
||||
"status": res.StatusCode,
|
||||
"statusText": res.Status,
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return makePromise(func() (any, error) {
|
||||
defer res.Body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -521,8 +524,9 @@ func (w termWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
type jsNetMap struct {
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
LockedOut bool `json:"lockedOut"`
|
||||
}
|
||||
|
||||
type jsNetMapNode struct {
|
||||
@@ -600,7 +604,7 @@ func generateHostname() string {
|
||||
// f is run on a goroutine and its return value is used to resolve the promise
|
||||
// (or reject it if an error is returned).
|
||||
func makePromise(f func() (any, error)) js.Value {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
resolve := args[0]
|
||||
reject := args[1]
|
||||
go func() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone --clone-only-type=OnlyGetClone
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded --clone-only-type=OnlyGetClone
|
||||
|
||||
type StructWithoutPtrs struct {
|
||||
Int int
|
||||
@@ -61,3 +61,8 @@ type StructWithSlices struct {
|
||||
type OnlyGetClone struct {
|
||||
SinViewerPorFavor bool
|
||||
}
|
||||
|
||||
type StructWithEmbedded struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}
|
||||
|
||||
@@ -211,3 +211,22 @@ func (src *OnlyGetClone) Clone() *OnlyGetClone {
|
||||
var _OnlyGetCloneCloneNeedsRegeneration = OnlyGetClone(struct {
|
||||
SinViewerPorFavor bool
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of StructWithEmbedded.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *StructWithEmbedded) Clone() *StructWithEmbedded {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(StructWithEmbedded)
|
||||
*dst = *src
|
||||
dst.A = src.A.Clone()
|
||||
dst.StructWithSlices = *src.StructWithSlices.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithEmbeddedCloneNeedsRegeneration = StructWithEmbedded(struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}{})
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded
|
||||
|
||||
// View returns a readonly view of StructWithPtrs.
|
||||
func (p *StructWithPtrs) View() StructWithPtrsView {
|
||||
@@ -325,3 +325,59 @@ var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithEmbedded.
|
||||
func (p *StructWithEmbedded) View() StructWithEmbeddedView {
|
||||
return StructWithEmbeddedView{ж: p}
|
||||
}
|
||||
|
||||
// StructWithEmbeddedView provides a read-only view over StructWithEmbedded.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type StructWithEmbeddedView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructWithEmbedded
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v StructWithEmbeddedView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v StructWithEmbeddedView) AsStruct() *StructWithEmbedded {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v StructWithEmbeddedView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *StructWithEmbeddedView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x StructWithEmbedded
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v StructWithEmbeddedView) A() StructWithPtrsView { return v.ж.A.View() }
|
||||
func (v StructWithEmbeddedView) StructWithSlices() StructWithSlicesView {
|
||||
return v.ж.StructWithSlices.View()
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithEmbeddedViewNeedsRegeneration = StructWithEmbedded(struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}{})
|
||||
|
||||
@@ -398,7 +398,7 @@ type maxMsgBuffer [maxMessageSize]byte
|
||||
|
||||
// bufPool holds the temporary buffers for Conn.Read & Write.
|
||||
var bufPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return new(maxMsgBuffer)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -47,7 +49,7 @@ var _ Client = (*Auto)(nil)
|
||||
// It's a concrete implementation of the Client interface.
|
||||
type Auto struct {
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
expiry *time.Time
|
||||
closed bool
|
||||
@@ -58,15 +60,17 @@ type Auto struct {
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
liteMapUpdateCancel context.CancelFunc // cancels a lite map update, may be nil
|
||||
liteMapUpdateCancels int // how many times we've canceled a lite map update
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -104,12 +108,12 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
if opts.Logf == nil {
|
||||
opts.Logf = func(fmt string, args ...any) {}
|
||||
}
|
||||
if opts.TimeNow == nil {
|
||||
opts.TimeNow = time.Now
|
||||
if opts.Clock == nil {
|
||||
opts.Clock = tstime.StdClock{}
|
||||
}
|
||||
c := &Auto{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
newMapCh: make(chan struct{}, 1),
|
||||
quit: make(chan struct{}),
|
||||
@@ -118,7 +122,11 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
statusFunc: opts.Status,
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
|
||||
@@ -163,28 +171,56 @@ func (c *Auto) Start() {
|
||||
func (c *Auto) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
|
||||
// If we're not already streaming a netmap, then tear down everything
|
||||
// and start a new stream (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || !c.loggedIn {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// If we are already in process of doing a LiteMapUpdate, cancel it and
|
||||
// try a new one. If this is the 10th time we have done this
|
||||
// cancelation, tear down everything and start again.
|
||||
const maxLiteMapUpdateAttempts = 10
|
||||
if c.inLiteMapUpdate {
|
||||
// Always cancel the in-flight lite map update, regardless of
|
||||
// whether we cancel the streaming map request or not.
|
||||
c.liteMapUpdateCancel()
|
||||
c.inLiteMapUpdate = false
|
||||
|
||||
if c.liteMapUpdateCancels >= maxLiteMapUpdateAttempts {
|
||||
// Not making progress
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// Increment our cancel counter and continue below to start a
|
||||
// new lite update.
|
||||
c.liteMapUpdateCancels++
|
||||
}
|
||||
|
||||
// Otherwise, send a lite update that doesn't keep a
|
||||
// long-running stream response.
|
||||
defer c.mu.Unlock()
|
||||
c.inLiteMapUpdate = true
|
||||
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
|
||||
c.liteMapUpdateCancel = cancel
|
||||
go func() {
|
||||
defer cancel()
|
||||
t0 := time.Now()
|
||||
t0 := c.clock.Now()
|
||||
err := c.direct.SendLiteMapUpdate(ctx)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
|
||||
c.mu.Lock()
|
||||
c.inLiteMapUpdate = false
|
||||
c.liteMapUpdateCancel = nil
|
||||
if err == nil {
|
||||
c.liteMapUpdateCancels = 0
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if err == nil {
|
||||
c.logf("[v1] successful lite map update in %v", d)
|
||||
return
|
||||
@@ -192,10 +228,13 @@ func (c *Auto) sendNewMapRequest() {
|
||||
if ctx.Err() == nil {
|
||||
c.logf("lite map update after %v: %v", d, err)
|
||||
}
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for any reason.
|
||||
c.cancelMapSafely()
|
||||
if !errors.Is(ctx.Err(), context.Canceled) {
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for reasons other than the context being
|
||||
// canceled.
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -206,6 +245,7 @@ func (c *Auto) cancelAuth() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -216,6 +256,8 @@ func (c *Auto) cancelMapLocked() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +271,12 @@ func (c *Auto) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Always reset our lite map cancels counter if we're canceling
|
||||
// everything, since we're about to restart with a new map update; this
|
||||
// allows future calls to sendNewMapRequest to retry sending lite
|
||||
// updates.
|
||||
c.liteMapUpdateCancels = 0
|
||||
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
@@ -360,7 +408,13 @@ func (c *Auto) authRoutine() {
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine-url", err, url, nil)
|
||||
bo.BackOff(ctx, err)
|
||||
if goal.url == url {
|
||||
// The server sent us the same URL we already tried,
|
||||
// backoff to avoid a busy loop.
|
||||
bo.BackOff(ctx, errors.New("login URL not changing"))
|
||||
} else {
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -498,6 +552,8 @@ func (c *Auto) mapRoutine() {
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
// Reset the backoff timer if we got a netmap.
|
||||
bo.BackOff(ctx, nil)
|
||||
})
|
||||
|
||||
health.SetInPollNetMap(false)
|
||||
@@ -649,14 +705,14 @@ func (c *Auto) Logout(ctx context.Context) error {
|
||||
c.mu.Unlock()
|
||||
c.cancelAuth()
|
||||
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
timer, timerChannel := c.clock.NewTimer(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
case <-timerChannel:
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
}
|
||||
@@ -717,7 +773,7 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
||||
}
|
||||
|
||||
func (c *Auto) TestOnlyTimeNow() time.Time {
|
||||
return c.timeNow()
|
||||
return c.clock.Now()
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
|
||||
@@ -20,7 +20,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user