Compare commits
893 Commits
v3.27.0
...
server-aut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0717578b06 | ||
|
|
6023eb1878 | ||
|
|
a1ece20617 | ||
|
|
0bc67b73a8 | ||
|
|
c7ab5bd34c | ||
|
|
843bf08aa1 | ||
|
|
5b25cc95a9 | ||
|
|
0fddbc54a2 | ||
|
|
11fcfb7d19 | ||
|
|
3cd7d7edcb | ||
|
|
30609b6fe9 | ||
|
|
8a0921748b | ||
|
|
3fac02a82a | ||
|
|
f11f142bee | ||
|
|
596faef8f2 | ||
|
|
3d1b6bc861 | ||
|
|
46ad576233 | ||
|
|
46beaac34b | ||
|
|
3025476e8b | ||
|
|
cd6f9493a4 | ||
|
|
9984ad22d7 | ||
|
|
3565ba67c4 | ||
|
|
ffb0bec4da | ||
|
|
4d2b8787e0 | ||
|
|
d4831ad4a6 | ||
|
|
9e1b53a732 | ||
|
|
d0113849d6 | ||
|
|
7b25fdfee8 | ||
|
|
5ed6e82922 | ||
|
|
7dbd14df27 | ||
|
|
96d8b53338 | ||
|
|
2bd19640d9 | ||
|
|
1047508bd7 | ||
|
|
eb49306b80 | ||
|
|
43da9ddbb3 | ||
|
|
7fbc5c3c07 | ||
|
|
e03f545e07 | ||
|
|
942f1f2c0f | ||
|
|
baf566d7a5 | ||
|
|
6712adfe6b | ||
|
|
2e2e5f9df5 | ||
|
|
35e9b2365d | ||
|
|
b0b769d2c1 | ||
|
|
d3c7d3c7bc | ||
|
|
65f49ea012 | ||
|
|
5687555921 | ||
|
|
0fb75036a0 | ||
|
|
2b513dd43d | ||
|
|
687d9b4736 | ||
|
|
c70c2ef932 | ||
|
|
af3ada109b | ||
|
|
9d40564734 | ||
|
|
3734815ada | ||
|
|
b9cc5c1fdc | ||
|
|
c646ca5766 | ||
|
|
1394be5143 | ||
|
|
93442526f8 | ||
|
|
d85402050b | ||
|
|
b1c62cb525 | ||
|
|
fae64a297a | ||
|
|
6e2682a9ce | ||
|
|
555049f09c | ||
|
|
712f7c3d35 | ||
|
|
7a51c211cd | ||
|
|
c48189c1c4 | ||
|
|
9803fa1cfd | ||
|
|
cf756f561a | ||
|
|
a4021fedc3 | ||
|
|
31a36a9250 | ||
|
|
36fe349b70 | ||
|
|
3ef1cfd97c | ||
|
|
669feb45f1 | ||
|
|
85890520ab | ||
|
|
340016521e | ||
|
|
ef523df42c | ||
|
|
5306e3bab1 | ||
|
|
72a49afd2b | ||
|
|
9b8edbb81e | ||
|
|
a1554feb3f | ||
|
|
490410bf09 | ||
|
|
8c113f5268 | ||
|
|
075cbd5a0f | ||
|
|
d82df2b431 | ||
|
|
a09f8214d9 | ||
|
|
396e9c003e | ||
|
|
b0c4a28be6 | ||
|
|
85325e4a31 | ||
|
|
9933dd3ec5 | ||
|
|
13532c8b4b | ||
|
|
3926797295 | ||
|
|
febd3f784f | ||
|
|
61b053f0e1 | ||
|
|
8dae352ccc | ||
|
|
e890c50da6 | ||
|
|
ddd9f4d021 | ||
|
|
7e58b4baee | ||
|
|
a21fbb9a4f | ||
|
|
3b7d27c919 | ||
|
|
68ddbfc0fe | ||
|
|
a2047cb800 | ||
|
|
fdd499146c | ||
|
|
37900341cf | ||
|
|
36bb368cad | ||
|
|
f9bdb219d0 | ||
|
|
0374c14e42 | ||
|
|
a035a151bd | ||
|
|
e69966381d | ||
|
|
94dfb2b1f2 | ||
|
|
92011205be | ||
|
|
c9707646bd | ||
|
|
c50705736b | ||
|
|
ec284c17f4 | ||
|
|
ad6c52dc4c | ||
|
|
5f182febae | ||
|
|
86d82c1098 | ||
|
|
842b9004da | ||
|
|
6ac7ca4f0f | ||
|
|
ddfcbe1bee | ||
|
|
88fd9388e4 | ||
|
|
69aafa53c9 | ||
|
|
3473fe9c15 | ||
|
|
c655500045 | ||
|
|
96a8015af6 | ||
|
|
ddd3876f92 | ||
|
|
f1f34722ee | ||
|
|
937c667ca8 | ||
|
|
3c45f57aaa | ||
|
|
30640eefe2 | ||
|
|
8567522594 | ||
|
|
bd8214e648 | ||
|
|
a61302f135 | ||
|
|
3dfb43e117 | ||
|
|
2388e0550b | ||
|
|
a7d70dd9a3 | ||
|
|
76a4bb5dc3 | ||
|
|
3daf15a612 | ||
|
|
81ffbaf057 | ||
|
|
abe9dcbe33 | ||
|
|
3c8e80a1a4 | ||
|
|
694988b32f | ||
|
|
ea31886299 | ||
|
|
5b2923ca65 | ||
|
|
432eaa6c04 | ||
|
|
5fd0af9395 | ||
|
|
03deb9aed0 | ||
|
|
cbdd1a933c | ||
|
|
99e9bc87cf | ||
|
|
9ef14ee070 | ||
|
|
7842ff4cdc | ||
|
|
3d6d03b327 | ||
|
|
7ebbaf4351 | ||
|
|
c665b13cec | ||
|
|
970b21a6eb | ||
|
|
62747f1eb8 | ||
|
|
a2e76e1683 | ||
|
|
07651683f9 | ||
|
|
429aea8e0f | ||
|
|
01fa9934bc | ||
|
|
ff7cadb43b | ||
|
|
540acc915d | ||
|
|
703a546c1d | ||
|
|
4851bd70da | ||
|
|
a2b3d7e30c | ||
|
|
4d60b71583 | ||
|
|
3f130931d2 | ||
|
|
946f055fed | ||
|
|
eaece0cb8e | ||
|
|
4203f4fabf | ||
|
|
c39edb6378 | ||
|
|
b3cc2781ff | ||
|
|
12c411e203 | ||
|
|
3bf937d705 | ||
|
|
bc55c25e73 | ||
|
|
897a9d7f57 | ||
|
|
4a128677dd | ||
|
|
9233f3f5ba | ||
|
|
11c2354408 | ||
|
|
1f2882434a | ||
|
|
01aaf2c86a | ||
|
|
d260ac7a49 | ||
|
|
0bea0d4ecd | ||
|
|
59994bd6e7 | ||
|
|
62799d2449 | ||
|
|
09c47c740c | ||
|
|
ecbfc02713 | ||
|
|
7be9288685 | ||
|
|
d1f57d0e36 | ||
|
|
74ea1a0f5a | ||
|
|
2a9ab29e7d | ||
|
|
8be78a5741 | ||
|
|
4a669c3458 | ||
|
|
ae5b71a864 | ||
|
|
6fff2ce1a4 | ||
|
|
f6165d206a | ||
|
|
8dbe7b8888 | ||
|
|
10f43d7a70 | ||
|
|
01283def17 | ||
|
|
b32e085354 | ||
|
|
ac9446e296 | ||
|
|
dea4080a7b | ||
|
|
2e63dba817 | ||
|
|
10384c9e37 | ||
|
|
34e8f5f3a9 | ||
|
|
ceb6ff4ca4 | ||
|
|
4c3da54303 | ||
|
|
5d75bbc869 | ||
|
|
72e227f87d | ||
|
|
c5c37e7f96 | ||
|
|
aaf3019d8c | ||
|
|
5191f3558f | ||
|
|
13ffffb157 | ||
|
|
7bc2972b27 | ||
|
|
ab08a5e666 | ||
|
|
8c730a6e4a | ||
|
|
4c47b6f142 | ||
|
|
264480b659 | ||
|
|
cb99f90bb5 | ||
|
|
2bf2525bc5 | ||
|
|
26705f5a23 | ||
|
|
ddbfdc9f14 | ||
|
|
9807d5f8f5 | ||
|
|
921992ebc7 | ||
|
|
8331ce6010 | ||
|
|
36c8da7ea7 | ||
|
|
73832d8b49 | ||
|
|
a03041cfea | ||
|
|
e7381b3800 | ||
|
|
9d50c23532 | ||
|
|
0501743814 | ||
|
|
06c9bc55d3 | ||
|
|
fe05521f2b | ||
|
|
93ed87d12b | ||
|
|
4218dba177 | ||
|
|
7872ab91dc | ||
|
|
c9e75bd697 | ||
|
|
7453f7f59a | ||
|
|
19a9ac9fd7 | ||
|
|
ecb06836b5 | ||
|
|
1e25372189 | ||
|
|
6042a9e3c2 | ||
|
|
fd4689ee70 | ||
|
|
4bd16373f2 | ||
|
|
ce642a6d8b | ||
|
|
ef6874fe57 | ||
|
|
29bc60bc35 | ||
|
|
fb145d68a0 | ||
|
|
6dd27e53d4 | ||
|
|
e0a977cf83 | ||
|
|
4d002a3ad6 | ||
|
|
4206859cad | ||
|
|
5dacbb994f | ||
|
|
ebf4bf9ea8 | ||
|
|
241a9930c9 | ||
|
|
f1e8200cfc | ||
|
|
03eddb1698 | ||
|
|
b25ee21e3e | ||
|
|
7e0738d113 | ||
|
|
0b078e5f5e | ||
|
|
45fe38e670 | ||
|
|
72e2e4b82c | ||
|
|
bdc594c297 | ||
|
|
1afe01d8cd | ||
|
|
234e54ac5c | ||
|
|
49b8f8b443 | ||
|
|
ce75c5ca21 | ||
|
|
e07966f71e | ||
|
|
c5395adfea | ||
|
|
9d1ec69b73 | ||
|
|
ee8802ee86 | ||
|
|
0d7115c832 | ||
|
|
08fb049f63 | ||
|
|
c87c0e12fe | ||
|
|
7b4befce61 | ||
|
|
6709a248d6 | ||
|
|
bf4cc0dabf | ||
|
|
982100782c | ||
|
|
4afbe9332f | ||
|
|
4019ee3ea1 | ||
|
|
e859c60343 | ||
|
|
8454123cae | ||
|
|
6b2f350ec9 | ||
|
|
e01ce9c6d8 | ||
|
|
ecc80a5a9e | ||
|
|
23b0320cfb | ||
|
|
3e79509c97 | ||
|
|
2185f347ce | ||
|
|
aa3ef5a1c2 | ||
|
|
acec050b95 | ||
|
|
9ca97fb04f | ||
|
|
4776948af6 | ||
|
|
4d9c619b24 | ||
|
|
62007bf1a1 | ||
|
|
7674efe8d7 | ||
|
|
b3ceece779 | ||
|
|
c74e4178bb | ||
|
|
c0621bf381 | ||
|
|
fb00fb16c2 | ||
|
|
6096b7ad4b | ||
|
|
9cb4c74493 | ||
|
|
e470dc8a12 | ||
|
|
ab49f1f733 | ||
|
|
62158a1739 | ||
|
|
3d16798544 | ||
|
|
b51aa0c6b9 | ||
|
|
84d00b42f1 | ||
|
|
e201856667 | ||
|
|
3254fc8aa6 | ||
|
|
4bca4ca932 | ||
|
|
a20695ffb3 | ||
|
|
d01cfef039 | ||
|
|
0eed558b10 | ||
|
|
423a5c37e0 | ||
|
|
cfca026621 | ||
|
|
6a6337b98f | ||
|
|
72b5afc771 | ||
|
|
659bc0c9cb | ||
|
|
827e591174 | ||
|
|
a369745101 | ||
|
|
586b0e17a0 | ||
|
|
b5f1055682 | ||
|
|
6b9c775055 | ||
|
|
d8b9b2a85b | ||
|
|
c826707d42 | ||
|
|
8a17cd87c3 | ||
|
|
f8da1e79bc | ||
|
|
cfc29d6a6b | ||
|
|
5467652b8b | ||
|
|
daa63c276d | ||
|
|
ab96acdc5b | ||
|
|
6e108706a1 | ||
|
|
4a6c229504 | ||
|
|
ed3a72790a | ||
|
|
4bf5777f23 | ||
|
|
f0f9bdb883 | ||
|
|
4984d90b5a | ||
|
|
b5e648d13a | ||
|
|
f71a1b083b | ||
|
|
75fd869625 | ||
|
|
657b4b787f | ||
|
|
32d6453918 | ||
|
|
c326b616b4 | ||
|
|
d5376629df | ||
|
|
3e825d7a08 | ||
|
|
059b12883f | ||
|
|
74aa509644 | ||
|
|
4105f74ce1 | ||
|
|
8318be3159 | ||
|
|
de196490db | ||
|
|
ab7d1ccf3d | ||
|
|
ed49a7a7c0 | ||
|
|
135832d985 | ||
|
|
1adbd9f692 | ||
|
|
26e1c92841 | ||
|
|
3c5b3514fb | ||
|
|
f884293f6e | ||
|
|
c67bd1aa2a | ||
|
|
77ace9377d | ||
|
|
6e676209ff | ||
|
|
80917d58b2 | ||
|
|
fa49f13f19 | ||
|
|
1fcabd152f | ||
|
|
385879c297 | ||
|
|
e0515cb458 | ||
|
|
1c43a1d55b | ||
|
|
6c639fcf7f | ||
|
|
ec1f252528 | ||
|
|
ee413f59a2 | ||
|
|
d4df87286e | ||
|
|
a194906bdd | ||
|
|
9b00763a69 | ||
|
|
4d627bb7b1 | ||
|
|
dc8fc5f81f | ||
|
|
b787e12e25 | ||
|
|
f96448947f | ||
|
|
e64e5af4c3 | ||
|
|
aa6dc786a4 | ||
|
|
84300db7c1 | ||
|
|
2ac0f35060 | ||
|
|
1a865f56d5 | ||
|
|
0406de399d | ||
|
|
71201411f4 | ||
|
|
c435bbb32c | ||
|
|
4cbfea41f2 | ||
|
|
f9c9ad34f7 | ||
|
|
4ea474b896 | ||
|
|
6aa4a93665 | ||
|
|
ea25a0ff89 | ||
|
|
659da67ed5 | ||
|
|
ffc6d2e593 | ||
|
|
03ce08e23d | ||
|
|
3449e7a0e1 | ||
|
|
c0062fb807 | ||
|
|
1ac031e78c | ||
|
|
e556871e8b | ||
|
|
082a38b769 | ||
|
|
39ae57f49d | ||
|
|
9024912e17 | ||
|
|
eecfb3952f | ||
|
|
0ebfe534d3 | ||
|
|
c5cc240a6c | ||
|
|
1a5a0148ea | ||
|
|
abe2aceb18 | ||
|
|
fa541b8fc2 | ||
|
|
a681d38dfb | ||
|
|
a7b96e3f4d | ||
|
|
04ef92edab | ||
|
|
919b55c3aa | ||
|
|
9c0f187a12 | ||
|
|
075a1e2a80 | ||
|
|
f31a846cda | ||
|
|
9bef46db77 | ||
|
|
d83217f7ac | ||
|
|
1cd2fec796 | ||
|
|
235f24ee5b | ||
|
|
2e34c6009e | ||
|
|
c0eb2f2315 | ||
|
|
8ad16cdc12 | ||
|
|
fae6544431 | ||
|
|
f8a41b2133 | ||
|
|
ff9b56d6d8 | ||
|
|
99d5a591b9 | ||
|
|
fbe252a9b6 | ||
|
|
76a92b90e3 | ||
|
|
2873b06275 | ||
|
|
9cdd6294d2 | ||
|
|
44bc60b00d | ||
|
|
6f0be57860 | ||
|
|
d3d8484b8e | ||
|
|
515ae8efb3 | ||
|
|
83826e1253 | ||
|
|
4292a500ae | ||
|
|
4a0f9c36ba | ||
|
|
ea1991496e | ||
|
|
4675572328 | ||
|
|
412921fc1f | ||
|
|
1c905d0e6f | ||
|
|
2ec9293324 | ||
|
|
9b39a301a8 | ||
|
|
cade2b99bf | ||
|
|
40cdb4f662 | ||
|
|
c58d6d4de2 | ||
|
|
0da2b6ad0b | ||
|
|
37f0e5c73b | ||
|
|
a9cd7be3f9 | ||
|
|
07459ee854 | ||
|
|
943943e8d1 | ||
|
|
5927ee9dec | ||
|
|
3b136e02db | ||
|
|
482447c151 | ||
|
|
5d8fbf8006 | ||
|
|
2ab80771d9 | ||
|
|
7399c00508 | ||
|
|
2d2f657851 | ||
|
|
0e21fdc9de | ||
|
|
b87b2109b1 | ||
|
|
2c30984a10 | ||
|
|
47593928f9 | ||
|
|
b961284845 | ||
|
|
b5d230d47a | ||
|
|
c2972f7bf6 | ||
|
|
aed235f52d | ||
|
|
bfe5e4380f | ||
|
|
eca182a32f | ||
|
|
caabaf918e | ||
|
|
d6924597dd | ||
|
|
c26476a2fd | ||
|
|
5be0d0bbba | ||
|
|
38ddcfa756 | ||
|
|
163ac48ce4 | ||
|
|
def407d610 | ||
|
|
22b2e2cc6e | ||
|
|
c92962e97c | ||
|
|
9d1a0b60a2 | ||
|
|
9cf2c9c4d2 | ||
|
|
e7150ba254 | ||
|
|
7ba70f19ef | ||
|
|
9488a9f88a | ||
|
|
020196f1c3 | ||
|
|
7e325715c7 | ||
|
|
75670a80b8 | ||
|
|
a43973c093 | ||
|
|
1827a03afd | ||
|
|
3100cc1e5e | ||
|
|
eed62fdc6d | ||
|
|
d2b8dbcb10 | ||
|
|
90d43856ef | ||
|
|
86f95cb390 | ||
|
|
3b807e2ca9 | ||
|
|
e8f2296a0d | ||
|
|
1dd38bc658 | ||
|
|
63303bc311 | ||
|
|
5200ee5722 | ||
|
|
86ec75722a | ||
|
|
0a29337c3b | ||
|
|
00ee6ff9a7 | ||
|
|
6d0a2a968f | ||
|
|
4bb77ebcc5 | ||
|
|
56ecfcb9f4 | ||
|
|
9a0fcbc011 | ||
|
|
b6c8399c3b | ||
|
|
7a88a09341 | ||
|
|
912b31cfc6 | ||
|
|
d21a943779 | ||
|
|
801a7fd6fe | ||
|
|
80053f6b7d | ||
|
|
e165bb6870 | ||
|
|
67bd1171ae | ||
|
|
4e2e46014d | ||
|
|
1693c59e0d | ||
|
|
9d4105ee59 | ||
|
|
19585da3bc | ||
|
|
51f830cfc1 | ||
|
|
804ea7ebd6 | ||
|
|
3294b8df60 | ||
|
|
d77ec7a6cb | ||
|
|
219d1f371c | ||
|
|
fa7fd5f076 | ||
|
|
d4f8eea7bf | ||
|
|
723d0f5e12 | ||
|
|
20f4d8cc0b | ||
|
|
64cca69bf3 | ||
|
|
fc8a2abb8f | ||
|
|
16ecf48b89 | ||
|
|
8fa4fd1b64 | ||
|
|
4db6d1ecf9 | ||
|
|
3b86927ca7 | ||
|
|
8bfa2f9b27 | ||
|
|
fe2a3e4d11 | ||
|
|
b0451d8e50 | ||
|
|
a0b9044fd3 | ||
|
|
c7a841f4b4 | ||
|
|
4ba159e483 | ||
|
|
63a696d7e7 | ||
|
|
d457342b46 | ||
|
|
c246dae2cc | ||
|
|
0f4a2e5224 | ||
|
|
db262050d5 | ||
|
|
227cdea0c8 | ||
|
|
33a6f1c01b | ||
|
|
f6f3c110f0 | ||
|
|
27a3f2c846 | ||
|
|
62169baeea | ||
|
|
4b18636a91 | ||
|
|
51432ca05f | ||
|
|
b5ebdcd040 | ||
|
|
416c1ee113 | ||
|
|
fe97e28461 | ||
|
|
cbd8711a21 | ||
|
|
7578e52ed5 | ||
|
|
0df68f76d5 | ||
|
|
9a528c42f8 | ||
|
|
5607916af6 | ||
|
|
4ad7a2a444 | ||
|
|
ab5dbdca97 | ||
|
|
a97fcda283 | ||
|
|
e955adc1e1 | ||
|
|
ac5141b411 | ||
|
|
f8c189e48a | ||
|
|
2f2a904c64 | ||
|
|
9261dca8ab | ||
|
|
7b5d5c3884 | ||
|
|
7c80d80904 | ||
|
|
ea40b84ec0 | ||
|
|
4e6ef649c4 | ||
|
|
dd40f1d2e6 | ||
|
|
490693bb26 | ||
|
|
c8d33ca5f3 | ||
|
|
e6df026332 | ||
|
|
7a30343053 | ||
|
|
fc02ae9c13 | ||
|
|
f70f0aca9c | ||
|
|
16acd1b162 | ||
|
|
2e3eb1fd7b | ||
|
|
a4cf17f81e | ||
|
|
c0a301611d | ||
|
|
cc934f5c68 | ||
|
|
74426f6202 | ||
|
|
03ed3cb1c8 | ||
|
|
1b1335835b | ||
|
|
5070dbcf7f | ||
|
|
90b9d85742 | ||
|
|
7a3b9941aa | ||
|
|
698095f0a0 | ||
|
|
5a06d8e155 | ||
|
|
7421dcb45f | ||
|
|
554a6cdb92 | ||
|
|
5aa39be973 | ||
|
|
192a7a56a3 | ||
|
|
1d1657e9be | ||
|
|
49b7301295 | ||
|
|
126804c15e | ||
|
|
a7643c6201 | ||
|
|
db2de5fc84 | ||
|
|
5c7b9aa6a1 | ||
|
|
63890c159e | ||
|
|
e7d5ae5dc1 | ||
|
|
b275354a92 | ||
|
|
ac02a64d17 | ||
|
|
9c80150e09 | ||
|
|
31a8bc9062 | ||
|
|
f15dde6502 | ||
|
|
f70609c464 | ||
|
|
c954e6f231 | ||
|
|
cb804577a9 | ||
|
|
e5be20d719 | ||
|
|
875690ab18 | ||
|
|
6a5aa8eddb | ||
|
|
7fdc7de210 | ||
|
|
dd7630997b | ||
|
|
aba5ca4536 | ||
|
|
7506625f40 | ||
|
|
5ddd703f6a | ||
|
|
71c51a7455 | ||
|
|
284d4340b1 | ||
|
|
2c1281d0a2 | ||
|
|
532df9f8d4 | ||
|
|
45b7da1058 | ||
|
|
907daff483 | ||
|
|
7757e8a114 | ||
|
|
e59e28152f | ||
|
|
2fe0594db7 | ||
|
|
794e96b449 | ||
|
|
07282f414c | ||
|
|
e583f9de47 | ||
|
|
8570e09eb9 | ||
|
|
ae5cba519c | ||
|
|
26f3832187 | ||
|
|
5989f29035 | ||
|
|
4ace99f318 | ||
|
|
d1c5e00df8 | ||
|
|
5eacb46226 | ||
|
|
6c17612310 | ||
|
|
fba73a0a0f | ||
|
|
4faef87c03 | ||
|
|
5914cb0e37 | ||
|
|
aa53436e56 | ||
|
|
8dfaebc737 | ||
|
|
062b6a276c | ||
|
|
647cd07de7 | ||
|
|
a530c84c5f | ||
|
|
0bb320065e | ||
|
|
d685d78e74 | ||
|
|
48896176e5 | ||
|
|
54dcf28b31 | ||
|
|
f8bf32bb34 | ||
|
|
748923021c | ||
|
|
a182e3503b | ||
|
|
991cfb8659 | ||
|
|
d0dfc21e2b | ||
|
|
617bd0c600 | ||
|
|
349b5429ba | ||
|
|
8db2944749 | ||
|
|
5986432a22 | ||
|
|
652daec509 | ||
|
|
f94d4b761a | ||
|
|
1ab74e6bb3 | ||
|
|
8e101d49a1 | ||
|
|
7c08e8f607 | ||
|
|
a4caa61c47 | ||
|
|
ebae167815 | ||
|
|
a6f00f2fb2 | ||
|
|
877617cc53 | ||
|
|
2800588ef7 | ||
|
|
f5efa42aaf | ||
|
|
10bd0e1505 | ||
|
|
a4c80b3045 | ||
|
|
dbb71bd695 | ||
|
|
a544f6e604 | ||
|
|
a18e026b70 | ||
|
|
0413a0a1ab | ||
|
|
cb6e9cb761 | ||
|
|
420ae40901 | ||
|
|
34e67f9f99 | ||
|
|
18c53aa597 | ||
|
|
6d2f9b9508 | ||
|
|
6826b05d58 | ||
|
|
9f959dbc6a | ||
|
|
87dbae5745 | ||
|
|
037f19e852 | ||
|
|
62ad8bcd8f | ||
|
|
2805c3388a | ||
|
|
535297dcf5 | ||
|
|
b3b6933ef4 | ||
|
|
edbbcc041a | ||
|
|
d430ebc34f | ||
|
|
0e9abc6e1d | ||
|
|
0c0dd10766 | ||
|
|
75454be6b6 | ||
|
|
4952e3b74e | ||
|
|
04b34a266c | ||
|
|
89b6a031b0 | ||
|
|
d4c6a9bdb5 | ||
|
|
cdc29d48b7 | ||
|
|
f4b464a7cf | ||
|
|
76690d3add | ||
|
|
9898387579 | ||
|
|
1ea15a1a13 | ||
|
|
bda6707685 | ||
|
|
89277828ac | ||
|
|
83b4a3fe55 | ||
|
|
45c9e780c0 | ||
|
|
33b8f5f596 | ||
|
|
447a7c9891 | ||
|
|
1bd355ab96 | ||
|
|
578ef768ab | ||
|
|
0378fe4a7b | ||
|
|
ebd94723c1 | ||
|
|
11b55abff3 | ||
|
|
7f32b43895 | ||
|
|
899f10c35e | ||
|
|
415cb7a945 | ||
|
|
e37f557cd5 | ||
|
|
79f213d97a | ||
|
|
11e1c9f9bb | ||
|
|
3ff3816d77 | ||
|
|
c0bdae8baf | ||
|
|
46e6bd16c9 | ||
|
|
5359257c65 | ||
|
|
5e659dc5b3 | ||
|
|
85e9d7d522 | ||
|
|
b71c8e58f4 | ||
|
|
e998372ce2 | ||
|
|
1216326867 | ||
|
|
f53f0cfffd | ||
|
|
f5f65d534a | ||
|
|
684cef6eab | ||
|
|
b4f6ae030d | ||
|
|
e95c94294f | ||
|
|
36b504609b | ||
|
|
1e6b4ed5eb | ||
|
|
0549326dfb | ||
|
|
87c6ebe1c5 | ||
|
|
f0afac243b | ||
|
|
53472077f4 | ||
|
|
55afdf33e1 | ||
|
|
d3c1f9263c | ||
|
|
6341d1dda6 | ||
|
|
e62e1883c2 | ||
|
|
501b98dbd3 | ||
|
|
029fd1da1f | ||
|
|
fd0267efef | ||
|
|
4414366370 | ||
|
|
08553bc90b | ||
|
|
6f850c4ad4 | ||
|
|
8e1316bd8a | ||
|
|
b345368257 | ||
|
|
90dd3b1b5c | ||
|
|
22455ac76f | ||
|
|
eb18eaf0a9 | ||
|
|
90c6c8485b | ||
|
|
381089ebdf | ||
|
|
292813831d | ||
|
|
991d75a1d0 | ||
|
|
d9dfb81cb4 | ||
|
|
67a9cacb61 | ||
|
|
a91eb95456 | ||
|
|
a295269518 | ||
|
|
42904b6749 | ||
|
|
364f9de756 | ||
|
|
7fd45cf17f | ||
|
|
eb71cfb144 | ||
|
|
48e469917e | ||
|
|
4bcd8ee9f5 | ||
|
|
1b2bcf901a | ||
|
|
306de8feda | ||
|
|
e3696f1eea | ||
|
|
7ff14a356c | ||
|
|
4bde50fb3a | ||
|
|
bd0868d764 | ||
|
|
5ffe8555ba | ||
|
|
78ccbb21cd | ||
|
|
92dbe1ebad | ||
|
|
2eec60cdd2 | ||
|
|
da8c104ebd | ||
|
|
0ef7b66047 | ||
|
|
e32d251cc1 | ||
|
|
9dd5e7bf1d | ||
|
|
b6de6035f6 | ||
|
|
88ccaf0b83 | ||
|
|
52c8bc075f | ||
|
|
2537cd5271 | ||
|
|
db91625de4 | ||
|
|
df78386fbe | ||
|
|
a1d70f740a | ||
|
|
187f42277a | ||
|
|
e1f89bb569 | ||
|
|
1d94f8ab2b | ||
|
|
045ecabb78 | ||
|
|
e6c3cb078a | ||
|
|
afa51b3ff6 | ||
|
|
f9c80b2285 | ||
|
|
fc5cf44b2c | ||
|
|
0c0f1663b1 | ||
|
|
306d8494d6 | ||
|
|
f5c00c3e2d | ||
|
|
ac9571c6b2 | ||
|
|
934fafb64b | ||
|
|
d51514015f | ||
|
|
a9cfd16d53 | ||
|
|
1a6f26fa3b | ||
|
|
0dd723b29f | ||
|
|
7ad6fc8e73 | ||
|
|
31c7e6362b | ||
|
|
072b42d867 | ||
|
|
5d66c193aa | ||
|
|
aa729515b9 | ||
|
|
54b7e23974 | ||
|
|
ad80e0c1ab | ||
|
|
5d7b278957 | ||
|
|
678caaf6a0 | ||
|
|
7228cd7b12 | ||
|
|
7b598a3534 | ||
|
|
9cdc9e9153 | ||
|
|
71ab0416b0 | ||
|
|
10a13bc8a7 | ||
|
|
be386a8e33 | ||
|
|
c33fb8bb97 | ||
|
|
20f20f051b | ||
|
|
179274ade0 | ||
|
|
84607e332b | ||
|
|
8186ef2342 | ||
|
|
19b184adba | ||
|
|
a97fd35d6e | ||
|
|
470ca020e2 | ||
|
|
f64d7c4343 | ||
|
|
c6f68a64e6 | ||
|
|
5aaa122460 | ||
|
|
de169c027f | ||
|
|
314c9663a2 | ||
|
|
21995eb3e3 | ||
|
|
6fc700bd62 | ||
|
|
acdbe2163e | ||
|
|
c3a231e0ab | ||
|
|
984e143336 | ||
|
|
e2ba2f82c0 | ||
|
|
ace5e97e68 | ||
|
|
82d42297e8 | ||
|
|
f99d5e8656 | ||
|
|
0795008c23 | ||
|
|
c975a86a70 | ||
|
|
69eee345d2 | ||
|
|
48afc05bcb | ||
|
|
39a62f5db7 | ||
|
|
006b218ade | ||
|
|
2b09b9c290 | ||
|
|
c42865b3d9 | ||
|
|
836f021a87 | ||
|
|
26b049b361 | ||
|
|
e75627365d | ||
|
|
ae0334c930 | ||
|
|
920ad8b54b | ||
|
|
ac4a4f83fc | ||
|
|
a4652c2d32 | ||
|
|
c40d4e075e | ||
|
|
95967136d3 | ||
|
|
576c1ee0c5 | ||
|
|
5d4032edf4 | ||
|
|
ff3f84f9fd | ||
|
|
2a19b68b9a | ||
|
|
ed6c010aff | ||
|
|
783fb38e41 | ||
|
|
fcab4ae3c6 | ||
|
|
a69c456965 | ||
|
|
0e6db2f1c5 | ||
|
|
7aab18d197 | ||
|
|
d6b39e66d1 | ||
|
|
3f5c72d898 | ||
|
|
691ade794b | ||
|
|
1693c4ed8a | ||
|
|
ae9b3279c3 | ||
|
|
04956e45c7 | ||
|
|
027664af7b | ||
|
|
f8d5f76bdf | ||
|
|
114f9be47f | ||
|
|
c73369e11c | ||
|
|
5603e25542 | ||
|
|
0d8cb66d43 | ||
|
|
e7e4cfca4c | ||
|
|
fd23f1a29b | ||
|
|
57481e3dd7 | ||
|
|
53952b143f | ||
|
|
e7b0f4c6be | ||
|
|
ea143c0c9a | ||
|
|
a951110461 | ||
|
|
7a8f5f53d5 | ||
|
|
1b585159d1 | ||
|
|
f3692cd47f | ||
|
|
15800fd4ff | ||
|
|
9fb085f361 | ||
|
|
1e3f878470 | ||
|
|
bcf9bfa5d3 | ||
|
|
56bdc1f0ae | ||
|
|
9de6428585 |
@@ -1,5 +1,4 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
devcontainer.json
|
devcontainer.json
|
||||||
docker-compose.yml
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
README.md
|
README.md
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FROM qmcgaw/godevcontainer
|
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||||
RUN apk add wireguard-tools
|
RUN apk add wireguard-tools htop openssl
|
||||||
|
|||||||
@@ -2,68 +2,47 @@
|
|||||||
|
|
||||||
Development container that can be used with VSCode.
|
Development container that can be used with VSCode.
|
||||||
|
|
||||||
It works on Linux, Windows and OSX.
|
It works on Linux, Windows (WSL2) and OSX.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [VS code](https://code.visualstudio.com/download) installed
|
- [VS code](https://code.visualstudio.com/download) installed
|
||||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||||
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
|
|
||||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
|
||||||
- Ensure your host has the following and that they are accessible by Docker:
|
|
||||||
- `~/.ssh` directory
|
|
||||||
- `~/.gitconfig` file (can be empty)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
1. Create the following files and directory on your host if you don't have them:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
touch ~/.gitconfig ~/.zsh_history
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
||||||
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
|
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
### Customize the image
|
For any customization to take effect, you should "rebuild and reopen":
|
||||||
|
|
||||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||||
|
2. Select `Dev-Containers: Rebuild Container`
|
||||||
|
|
||||||
```Dockerfile
|
Changes you can make are notably:
|
||||||
FROM qmcgaw/godevcontainer
|
|
||||||
USER root
|
|
||||||
RUN apk add curl
|
|
||||||
USER vscode
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
|
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
||||||
|
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
||||||
|
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
||||||
|
|
||||||
To rebuild the image, either:
|
```json
|
||||||
|
// Welcome script
|
||||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
{
|
||||||
- With a terminal, go to this directory and `docker-compose build`
|
"source": "/yourpath/.welcome.sh",
|
||||||
|
"target": "/root/.welcome.sh",
|
||||||
### Customize VS code settings
|
"type": "bind"
|
||||||
|
},
|
||||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
|
||||||
|
|
||||||
### Entrypoint script
|
|
||||||
|
|
||||||
You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
|
|
||||||
|
|
||||||
### Publish a port
|
|
||||||
|
|
||||||
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
|
|
||||||
|
|
||||||
### Run other services
|
|
||||||
|
|
||||||
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
database:
|
|
||||||
image: postgres
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: password
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
|
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||||
1. In the VS code command palette, rebuild the container.
|
|
||||||
|
|||||||
@@ -1,81 +1,111 @@
|
|||||||
{
|
{
|
||||||
"name": "gluetun-dev",
|
"name": "gluetun-dev",
|
||||||
"dockerComposeFile": [
|
// User defined settings
|
||||||
"docker-compose.yml"
|
"containerEnv": {
|
||||||
],
|
"TZ": ""
|
||||||
"service": "vscode",
|
},
|
||||||
"runServices": [
|
// Fixed settings
|
||||||
"vscode"
|
"build": {
|
||||||
],
|
"dockerfile": "./Dockerfile"
|
||||||
"shutdownAction": "stopCompose",
|
},
|
||||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||||
"workspaceFolder": "/workspace",
|
"capAdd": [
|
||||||
"extensions": [
|
"NET_ADMIN", // Gluetun specific
|
||||||
"golang.go",
|
"SYS_PTRACE" // for dlv Go debugging
|
||||||
"eamodio.gitlens", // IDE Git information
|
],
|
||||||
"davidanson.vscode-markdownlint",
|
"securityOpt": [
|
||||||
"ms-azuretools.vscode-docker", // Docker integration and linting
|
"seccomp=unconfined" // for dlv Go debugging
|
||||||
"shardulm94.trailing-spaces", // Show trailing spaces
|
],
|
||||||
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
"mounts": [
|
||||||
"bierner.emojisense", // Emoji sense for markdown
|
// Zsh commands history persistence
|
||||||
"stkb.rewrap", // rewrap comments after n characters on one line
|
{
|
||||||
"vscode-icons-team.vscode-icons", // Better file extension icons
|
"source": "${localEnv:HOME}/.zsh_history",
|
||||||
"github.vscode-pull-request-github", // Github interaction
|
"target": "/root/.zsh_history",
|
||||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
"type": "bind"
|
||||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
},
|
||||||
"IBM.output-colorizer", // Colorize your output/test logs
|
// Git configuration file
|
||||||
"mohsen1.prettify-json", // Prettify JSON data
|
{
|
||||||
],
|
"source": "${localEnv:HOME}/.gitconfig",
|
||||||
"settings": {
|
"target": "/root/.gitconfig",
|
||||||
"files.eol": "\n",
|
"type": "bind"
|
||||||
"remote.extensionKind": {
|
},
|
||||||
"ms-azuretools.vscode-docker": "workspace"
|
// SSH directory for Linux, OSX and WSL
|
||||||
},
|
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||||
"editor.codeActionsOnSaveTimeout": 3000,
|
// created in the container. On Windows, files are copied
|
||||||
"go.useLanguageServer": true,
|
// from /mnt/ssh to ~/.ssh to fix permissions.
|
||||||
"[go]": {
|
{
|
||||||
"editor.formatOnSave": true,
|
"source": "${localEnv:HOME}/.ssh",
|
||||||
"editor.codeActionsOnSave": {
|
"target": "/mnt/ssh",
|
||||||
"source.organizeImports": true,
|
"type": "bind"
|
||||||
},
|
},
|
||||||
// Optional: Disable snippets, as they conflict with completion ranking.
|
// Docker socket to access the host Docker server
|
||||||
"editor.snippetSuggestions": "none"
|
{
|
||||||
},
|
"source": "/var/run/docker.sock",
|
||||||
"[go.mod]": {
|
"target": "/var/run/docker.sock",
|
||||||
"editor.formatOnSave": true,
|
"type": "bind"
|
||||||
"editor.codeActionsOnSave": {
|
}
|
||||||
"source.organizeImports": true,
|
],
|
||||||
},
|
"customizations": {
|
||||||
},
|
"vscode": {
|
||||||
"gopls": {
|
"extensions": [
|
||||||
"usePlaceholders": false,
|
"golang.go",
|
||||||
"staticcheck": true
|
"eamodio.gitlens", // IDE Git information
|
||||||
},
|
"davidanson.vscode-markdownlint",
|
||||||
"go.autocompleteUnimportedPackages": true,
|
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||||
"go.gotoSymbol.includeImports": true,
|
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||||
"go.gotoSymbol.includeGoroot": true,
|
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||||
"go.lintTool": "golangci-lint",
|
"bierner.emojisense", // Emoji sense for markdown
|
||||||
"go.buildOnSave": "workspace",
|
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||||
"go.lintOnSave": "workspace",
|
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||||
"go.vetOnSave": "workspace",
|
"github.vscode-pull-request-github", // Github interaction
|
||||||
"editor.formatOnSave": true,
|
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||||
"go.toolsEnvVars": {
|
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||||
"GOFLAGS": "-tags=",
|
"IBM.output-colorizer", // Colorize your output/test logs
|
||||||
// "CGO_ENABLED": 1 // for the race detector
|
"github.copilot" // AI code completion
|
||||||
},
|
],
|
||||||
"gopls.env": {
|
"settings": {
|
||||||
"GOFLAGS": "-tags="
|
"files.eol": "\n",
|
||||||
},
|
"remote.extensionKind": {
|
||||||
"go.testEnvVars": {
|
"ms-azuretools.vscode-docker": "workspace"
|
||||||
"": ""
|
},
|
||||||
},
|
"go.useLanguageServer": true,
|
||||||
"go.testFlags": [
|
"[go]": {
|
||||||
"-v",
|
"editor.codeActionsOnSave": {
|
||||||
// "-race"
|
"source.organizeImports": "explicit"
|
||||||
],
|
}
|
||||||
"go.testTimeout": "10s",
|
},
|
||||||
"go.coverOnSingleTest": true,
|
"[go.mod]": {
|
||||||
"go.coverOnSingleTestFile": true,
|
"editor.codeActionsOnSave": {
|
||||||
"go.coverOnTestPackage": true
|
"source.organizeImports": "explicit"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"gopls": {
|
||||||
|
"usePlaceholders": false,
|
||||||
|
"staticcheck": true,
|
||||||
|
"ui.diagnostic.analyses": {
|
||||||
|
"ST1000": false
|
||||||
|
},
|
||||||
|
"formatting.gofumpt": true,
|
||||||
|
},
|
||||||
|
"go.lintTool": "golangci-lint",
|
||||||
|
"go.lintOnSave": "package",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"go.buildTags": "linux",
|
||||||
|
"go.toolsEnvVars": {
|
||||||
|
"CGO_ENABLED": "0"
|
||||||
|
},
|
||||||
|
"go.testEnvVars": {
|
||||||
|
"CGO_ENABLED": "1"
|
||||||
|
},
|
||||||
|
"go.testFlags": [
|
||||||
|
"-v",
|
||||||
|
"-race"
|
||||||
|
],
|
||||||
|
"go.testTimeout": "10s",
|
||||||
|
"go.coverOnSingleTest": true,
|
||||||
|
"go.coverOnSingleTestFile": true,
|
||||||
|
"go.coverOnTestPackage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
|
|
||||||
services:
|
|
||||||
vscode:
|
|
||||||
build: .
|
|
||||||
image: godevcontainer
|
|
||||||
devices:
|
|
||||||
- /dev/net/tun:/dev/net/tun
|
|
||||||
volumes:
|
|
||||||
- ../:/workspace
|
|
||||||
# Docker socket to access Docker server
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
# Docker configuration
|
|
||||||
- ~/.docker:/root/.docker:z
|
|
||||||
# SSH directory for Linux, OSX and WSL
|
|
||||||
- ~/.ssh:/root/.ssh:z
|
|
||||||
# For Windows without WSL, a copy will be made
|
|
||||||
# from /tmp/.ssh to ~/.ssh to fix permissions
|
|
||||||
#- ~/.ssh:/tmp/.ssh:ro
|
|
||||||
# Shell history persistence
|
|
||||||
- ~/.zsh_history:/root/.zsh_history:z
|
|
||||||
# Git config
|
|
||||||
- ~/.gitconfig:/root/.gitconfig:z
|
|
||||||
environment:
|
|
||||||
- TZ=
|
|
||||||
cap_add:
|
|
||||||
# For debugging with dlv
|
|
||||||
# - SYS_PTRACE
|
|
||||||
- NET_ADMIN
|
|
||||||
security_opt:
|
|
||||||
# For debugging with dlv
|
|
||||||
- seccomp:unconfined
|
|
||||||
entrypoint: zsh -c "while sleep 1000; do :; done"
|
|
||||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
|
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
|
||||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||||
|
|||||||
18
.github/ISSUE_TEMPLATE/bug.yml
vendored
18
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -7,13 +7,18 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
|
||||||
|
- you do not fill out **the title of the issue** ☝️
|
||||||
|
- you do not provide the **Gluetun version** as requested below
|
||||||
|
- you provide **less than 10 lines of logs** as requested below
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: urgent
|
id: urgent
|
||||||
attributes:
|
attributes:
|
||||||
label: Is this urgent?
|
label: Is this urgent?
|
||||||
description: |
|
description: |
|
||||||
Is this a critical bug, or do you need this fixed urgently?
|
Is this a critical bug, or do you need this fixed urgently?
|
||||||
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help.
|
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
|
||||||
options:
|
options:
|
||||||
- "No"
|
- "No"
|
||||||
- "Yes"
|
- "Yes"
|
||||||
@@ -40,10 +45,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: VPN service provider
|
label: VPN service provider
|
||||||
options:
|
options:
|
||||||
|
- AirVPN
|
||||||
- Custom
|
- Custom
|
||||||
- Cyberghost
|
- Cyberghost
|
||||||
- ExpressVPN
|
- ExpressVPN
|
||||||
- FastestVPN
|
- FastestVPN
|
||||||
|
- Giganews
|
||||||
- HideMyAss
|
- HideMyAss
|
||||||
- IPVanish
|
- IPVanish
|
||||||
- IVPN
|
- IVPN
|
||||||
@@ -54,8 +61,10 @@ body:
|
|||||||
- PrivateVPN
|
- PrivateVPN
|
||||||
- ProtonVPN
|
- ProtonVPN
|
||||||
- PureVPN
|
- PureVPN
|
||||||
|
- SlickVPN
|
||||||
- Surfshark
|
- Surfshark
|
||||||
- TorGuard
|
- TorGuard
|
||||||
|
- VPNSecure.me
|
||||||
- VPNUnlimited
|
- VPNUnlimited
|
||||||
- VyprVPN
|
- VyprVPN
|
||||||
- WeVPN
|
- WeVPN
|
||||||
@@ -72,6 +81,7 @@ body:
|
|||||||
- Portainer
|
- Portainer
|
||||||
- Kubernetes
|
- Kubernetes
|
||||||
- Podman
|
- Podman
|
||||||
|
- Unraid
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -81,7 +91,7 @@ body:
|
|||||||
label: What is the version of Gluetun
|
label: What is the version of Gluetun
|
||||||
description: |
|
description: |
|
||||||
Copy paste the version line at the top of your logs.
|
Copy paste the version line at the top of your logs.
|
||||||
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -94,9 +104,9 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Share your logs
|
label: Share your logs (at least 10 lines)
|
||||||
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
||||||
render: log
|
render: plain text
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,6 +1,10 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: Report a Wiki issue
|
||||||
|
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
|
||||||
|
about: Please create an issue on the gluetun-wiki repository.
|
||||||
- name: Configuration help?
|
- name: Configuration help?
|
||||||
url: https://github.com/qdm12/gluetun/discussions/new
|
url: https://github.com/qdm12/gluetun/discussions/new/choose
|
||||||
about: Please create a Github discussion.
|
about: Please create a Github discussion.
|
||||||
- name: Unraid template issue
|
- name: Unraid template issue
|
||||||
url: https://github.com/qdm12/gluetun/discussions/550
|
url: https://github.com/qdm12/gluetun/discussions/550
|
||||||
|
|||||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
One of the following is required:
|
Important notes:
|
||||||
|
|
||||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||||
|
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
|
|
||||||
|
## For Wireguard
|
||||||
|
|
||||||
|
Wireguard can be natively supported ONLY if:
|
||||||
|
|
||||||
|
- the `PrivateKey` field value is the same across all servers for one user account
|
||||||
|
- the `Address` field value is:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||||
|
- the same across all servers for one user account
|
||||||
|
- the `PublicKey` field value is:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||||
|
- the same across all servers for one user account
|
||||||
|
- the `Endpoint` field value:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||||
|
- can be determined using a pattern, for example using country codes in hostnames
|
||||||
|
|
||||||
|
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||||
|
|
||||||
|
If **all** of these conditions are met, please provide an answer for each of them.
|
||||||
|
|
||||||
|
## For OpenVPN
|
||||||
|
|
||||||
|
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||||
|
|
||||||
|
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||||
|
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||||
|
|
||||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
|
||||||
you can only use a custom Openvpn configuration file.
|
|
||||||
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.
|
|
||||||
|
|||||||
18
.github/ISSUE_TEMPLATE/wiki issue.yml
vendored
18
.github/ISSUE_TEMPLATE/wiki issue.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: Wiki issue
|
|
||||||
description: Report a Wiki issue
|
|
||||||
title: "Wiki issue: "
|
|
||||||
labels: ["📄 Wiki issue"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: url
|
|
||||||
attributes:
|
|
||||||
label: "URL to the Wiki page"
|
|
||||||
placeholder: "https://github.com/qdm12/gluetun/wiki/OpenVPN-options"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: "What's the issue?"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
198
.github/labels.yml
vendored
198
.github/labels.yml
vendored
@@ -1,110 +1,152 @@
|
|||||||
- name: "Bug :bug:"
|
- name: "Status: 🗯️ Waiting for feedback"
|
||||||
color: "b60205"
|
color: "f7d692"
|
||||||
description: ""
|
- name: "Status: 🔴 Blocked"
|
||||||
- name: "Feature request :bulb:"
|
color: "f7d692"
|
||||||
color: "0e8a16"
|
description: "Blocked by another issue or pull request"
|
||||||
description: ""
|
- name: "Status: 📌 Before next release"
|
||||||
- name: "Help wanted :pray:"
|
color: "f7d692"
|
||||||
color: "4caf50"
|
description: "Has to be done before the next release"
|
||||||
description: ""
|
- name: "Status: 🔒 After next release"
|
||||||
- name: "Documentation :memo:"
|
color: "f7d692"
|
||||||
color: "c5def5"
|
description: "Will be done after the next release"
|
||||||
description: ""
|
- name: "Status: 🟡 Nearly resolved"
|
||||||
- name: "Needs more info :thinking:"
|
color: "f7d692"
|
||||||
color: "795548"
|
description: "This might be resolved or is about to be resolved"
|
||||||
description: ""
|
|
||||||
|
|
||||||
# Priority
|
- name: "Closed: ⚰️ Inactive"
|
||||||
- name: "🚨 Urgent"
|
color: "959a9c"
|
||||||
color: "d5232f"
|
description: "No answer was received for weeks"
|
||||||
description: ""
|
- name: "Closed: 👥 Duplicate"
|
||||||
- name: "💤 Low priority"
|
color: "959a9c"
|
||||||
color: "4285f4"
|
description: "Issue duplicates an existing issue"
|
||||||
description: ""
|
- name: "Closed: 🗑️ Bad issue"
|
||||||
|
color: "959a9c"
|
||||||
|
- name: "Closed: ☠️ cannot be done"
|
||||||
|
color: "959a9c"
|
||||||
|
|
||||||
|
- name: "Priority: 🚨 Urgent"
|
||||||
|
color: "03adfc"
|
||||||
|
- name: "Priority: 💤 Low priority"
|
||||||
|
color: "03adfc"
|
||||||
|
|
||||||
|
- name: "Complexity: ☣️ Hard to do"
|
||||||
|
color: "ff9efc"
|
||||||
|
- name: "Complexity: 🟩 Easy to do"
|
||||||
|
color: "ff9efc"
|
||||||
|
|
||||||
|
- name: "Popularity: ❤️🔥 extreme"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Popularity: ❤️ high"
|
||||||
|
color: "ffc7ea"
|
||||||
|
|
||||||
# VPN providers
|
# VPN providers
|
||||||
- name: ":cloud: Cyberghost"
|
- name: "☁️ AirVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Custom"
|
||||||
- name: ":cloud: HideMyAss"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Cyberghost"
|
||||||
- name: ":cloud: IPVanish"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Giganews"
|
||||||
- name: ":cloud: IVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ HideMyAss"
|
||||||
- name: ":cloud: ExpressVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ IPVanish"
|
||||||
- name: ":cloud: FastestVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ IVPN"
|
||||||
- name: ":cloud: Mullvad"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ ExpressVPN"
|
||||||
- name: ":cloud: NordVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ FastestVPN"
|
||||||
- name: ":cloud: Perfect Privacy"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Mullvad"
|
||||||
- name: ":cloud: PIA"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ NordVPN"
|
||||||
- name: ":cloud: Privado"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Perfect Privacy"
|
||||||
- name: ":cloud: PrivateVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ PIA"
|
||||||
- name: ":cloud: ProtonVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: ":cloud: PureVPN"
|
- name: "☁️ Privado"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ PrivateVPN"
|
||||||
- name: ":cloud: Surfshark"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ ProtonVPN"
|
||||||
- name: ":cloud: Torguard"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ PureVPN"
|
||||||
- name: ":cloud: VPNUnlimited"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ SlickVPN"
|
||||||
- name: ":cloud: Vyprvpn"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Surfshark"
|
||||||
- name: ":cloud: WeVPN"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Torguard"
|
||||||
- name: ":cloud: Windscribe"
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ VPNSecure.me"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ VPNUnlimited"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Vyprvpn"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ WeVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Windscribe"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
description: ""
|
|
||||||
|
|
||||||
# Problem category
|
- name: "Category: User error 🤦"
|
||||||
- name: "Openvpn"
|
from_name: "Category: Config problem 📝"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Healthcheck 🩺"
|
||||||
- name: "Wireguard"
|
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Documentation ✒️"
|
||||||
- name: "Unbound (DNS over TLS)"
|
description: "A problem with the readme or a code comment."
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Maintenance ⛓️"
|
||||||
- name: "Firewall"
|
description: "Anything related to code or other maintenance"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Logs 📚"
|
||||||
- name: "HTTP proxy"
|
description: "Something to change in logs"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Good idea 🎯"
|
||||||
- name: "Shadowsocks"
|
description: "This is a good idea, judged by the maintainers"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Motivated! 🙌"
|
||||||
- name: "Healthcheck server"
|
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Category: Foolproof settings 👼"
|
||||||
- name: "Control server"
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Label missing ❗"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: updater ♻️"
|
||||||
|
color: "ffc7ea"
|
||||||
|
description: "Concerns the code to update servers data"
|
||||||
|
- name: "Category: New provider 🆕"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: OpenVPN 🔐"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Wireguard 🔐"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: DNS 📠"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Firewall ⛓️"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Routing 🛤️"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: IPv6 🛰️"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: VPN port forwarding 📥"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: HTTP proxy 🔁"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Shadowsocks 🔁"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: control server ⚙️"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: kernel 🧠"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: public IP service 💬"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: servers storage 📦"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Performance 🚀"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Investigation 🔍"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
description: ""
|
|
||||||
|
|||||||
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Description
|
||||||
|
|
||||||
|
<!-- Please describe the reason for the changes being proposed. -->
|
||||||
|
|
||||||
|
# Issue
|
||||||
|
|
||||||
|
<!-- Please link to the issue(s) this change relates to. -->
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
|
||||||
|
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
|
||||||
|
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
||||||
35
.github/workflows/ci-skip.yml
vendored
Normal file
35
.github/workflows/ci-skip.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: No trigger file paths
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- .github/workflows/ci.yml
|
||||||
|
- cmd/**
|
||||||
|
- internal/**
|
||||||
|
- pkg/**
|
||||||
|
- .dockerignore
|
||||||
|
- .golangci.yml
|
||||||
|
- Dockerfile
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- .github/workflows/ci.yml
|
||||||
|
- cmd/**
|
||||||
|
- internal/**
|
||||||
|
- pkg/**
|
||||||
|
- .dockerignore
|
||||||
|
- .golangci.yml
|
||||||
|
- Dockerfile
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: No trigger path triggered for required verify workflow.
|
||||||
|
run: exit 0
|
||||||
145
.github/workflows/ci.yml
vendored
145
.github/workflows/ci.yml
vendored
@@ -17,8 +17,6 @@ on:
|
|||||||
- go.mod
|
- go.mod
|
||||||
- go.sum
|
- go.sum
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/ci.yml
|
- .github/workflows/ci.yml
|
||||||
- cmd/**
|
- cmd/**
|
||||||
@@ -32,23 +30,28 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
|
|
||||||
if: |
|
|
||||||
github.actor != 'dependabot[bot]' &&
|
|
||||||
(github.event_name == 'push' ||
|
|
||||||
github.event_name == 'release' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: "1"
|
DOCKER_BUILDKIT: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: reviewdog/action-misspell@v1
|
||||||
|
with:
|
||||||
|
locale: "US"
|
||||||
|
level: error
|
||||||
|
exclude: |
|
||||||
|
./internal/storage/servers.json
|
||||||
|
*.md
|
||||||
|
|
||||||
- name: Linting
|
- name: Linting
|
||||||
run: docker build --target lint .
|
run: docker build --target lint .
|
||||||
|
|
||||||
- name: Go mod tidy check
|
- name: Mocks check
|
||||||
run: docker build --target tidy .
|
run: docker build --target mocks .
|
||||||
|
|
||||||
- name: Build test image
|
- name: Build test image
|
||||||
run: docker build --target test -t test-container .
|
run: docker build --target test -t test-container .
|
||||||
@@ -56,83 +59,113 @@ jobs:
|
|||||||
- name: Run tests in test container
|
- name: Run tests in test container
|
||||||
run: |
|
run: |
|
||||||
touch coverage.txt
|
touch coverage.txt
|
||||||
docker run --rm \
|
docker run --rm --device /dev/net/tun \
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
test-container
|
test-container
|
||||||
|
|
||||||
- name: Code security analysis
|
|
||||||
uses: snyk/actions/golang@master
|
|
||||||
env:
|
|
||||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build final image
|
- name: Build final image
|
||||||
run: docker build -t final-image .
|
run: docker build -t final-image .
|
||||||
|
|
||||||
# - name: Image security analysis
|
verify-private:
|
||||||
# uses: snyk/actions/docker@master
|
|
||||||
# env:
|
|
||||||
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# image: final-image
|
|
||||||
|
|
||||||
publish:
|
|
||||||
# Only run if it's a push event or if it's a PR from this repository
|
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'push' ||
|
github.repository == 'qdm12/gluetun' &&
|
||||||
github.event_name == 'release' ||
|
(
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'release' ||
|
||||||
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
|
)
|
||||||
needs: [verify]
|
needs: [verify]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: secrets
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v1
|
- run: docker build -t qmcgaw/gluetun .
|
||||||
- uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- uses: docker/login-action@v1
|
- name: Setup Go for CI utility
|
||||||
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
username: qmcgaw
|
go-version-file: ci/go.mod
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Check for semver tag
|
- name: Build utility
|
||||||
id: semvercheck
|
run: go build -C ./ci -o runner ./cmd/main.go
|
||||||
run: |
|
|
||||||
if [[ ${{ github.ref }} =~ ^refs\/tags\/v0\.[0-9]+\.[0-9]+$ ]]; then
|
- name: Run Gluetun container with Mullvad configuration
|
||||||
MATCH=true
|
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
||||||
else
|
|
||||||
MATCH=false
|
codeql:
|
||||||
fi
|
runs-on: ubuntu-latest
|
||||||
if [[ ${{ github.ref }} =~ ^refs\/tags\/v[1-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
permissions:
|
||||||
MATCH=$MATCH_nonzero
|
actions: read
|
||||||
fi
|
contents: read
|
||||||
echo ::set-output name=match::$MATCH
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
- uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: go
|
||||||
|
- uses: github/codeql-action/autobuild@v4
|
||||||
|
- uses: github/codeql-action/analyze@v4
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: |
|
||||||
|
github.repository == 'qdm12/gluetun' &&
|
||||||
|
(
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'release' ||
|
||||||
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
|
)
|
||||||
|
needs: [verify, codeql]
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# extract metadata (tags, labels) for Docker
|
# extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ github.ref == 'refs/heads/master' }}
|
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||||
images: |
|
images: |
|
||||||
|
ghcr.io/qdm12/gluetun
|
||||||
qmcgaw/gluetun
|
qmcgaw/gluetun
|
||||||
qmcgaw/private-internet-access
|
qmcgaw/private-internet-access
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch,enable=${{ github.ref != 'refs/heads/master' }}
|
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=ref,event=tag,enable=${{ !startsWith(steps.semvercheck.outputs.match, 'true') }}
|
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
||||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true') }}
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
type=semver,pattern=v{{major}}.{{minor}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true') }}
|
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
|
||||||
type=semver,pattern=v{{major}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true_nonzero') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||||
type=raw,value=latest,enable=${{ !startsWith(steps.semvercheck.outputs.match, 'true') }}
|
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: qmcgaw
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: qdm12
|
||||||
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Short commit
|
- name: Short commit
|
||||||
id: shortcommit
|
id: shortcommit
|
||||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Build and push final image
|
- name: Build and push final image
|
||||||
uses: docker/build-push-action@v2.7.0
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
21
.github/workflows/closed-issue.yml
vendored
Normal file
21
.github/workflows/closed-issue.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Closed issue
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
|
||||||
|
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
|
||||||
|
|
||||||
|
This is an automated comment setup because @qdm12 is the sole maintainer of this project
|
||||||
|
which became too popular to monitor issues closed.
|
||||||
14
.github/workflows/configs/mlc-config.json
vendored
Normal file
14
.github/workflows/configs/mlc-config.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"ignorePatterns": [
|
||||||
|
{
|
||||||
|
"pattern": "^https://console.substack.com/p/console-72$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeout": "20s",
|
||||||
|
"retryOn429": false,
|
||||||
|
"fallbackRetryDelay": "30s",
|
||||||
|
"aliveStatusCodes": [
|
||||||
|
200,
|
||||||
|
429
|
||||||
|
]
|
||||||
|
}
|
||||||
37
.github/workflows/dependabot.yml
vendored
37
.github/workflows/dependabot.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Dependabot
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- .github/workflows/dependabot.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
verify:
|
|
||||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
DOCKER_BUILDKIT: "1"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.4.0
|
|
||||||
|
|
||||||
- name: Build test image
|
|
||||||
run: docker build --target test -t test-container .
|
|
||||||
|
|
||||||
- name: Run tests in test container
|
|
||||||
run: |
|
|
||||||
touch coverage.txt
|
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
|
||||||
test-container
|
|
||||||
|
|
||||||
- name: Build final image
|
|
||||||
run: docker build -t final-image .
|
|
||||||
21
.github/workflows/dockerhub-description.yml
vendored
21
.github/workflows/dockerhub-description.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Docker Hub description
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
paths:
|
|
||||||
- README.md
|
|
||||||
- .github/workflows/dockerhub-description.yml
|
|
||||||
jobs:
|
|
||||||
dockerHubDescription:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.4.0
|
|
||||||
- name: Docker Hub Description
|
|
||||||
uses: peter-evans/dockerhub-description@v2
|
|
||||||
with:
|
|
||||||
username: qmcgaw
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
repository: qmcgaw/gluetun
|
|
||||||
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
|
||||||
readme-filepath: README.md
|
|
||||||
40
.github/workflows/fork.yml
vendored
40
.github/workflows/fork.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Fork
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- .github/workflows/fork.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
verify:
|
|
||||||
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'dependabot[bot]'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
DOCKER_BUILDKIT: "1"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.4.0
|
|
||||||
|
|
||||||
- name: Linting
|
|
||||||
run: docker build --target lint .
|
|
||||||
|
|
||||||
- name: Build test image
|
|
||||||
run: docker build --target test -t test-container .
|
|
||||||
|
|
||||||
- name: Run tests in test container
|
|
||||||
run: |
|
|
||||||
touch coverage.txt
|
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
|
||||||
test-container
|
|
||||||
|
|
||||||
- name: Build final image
|
|
||||||
run: docker build -t final-image .
|
|
||||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -7,9 +7,11 @@ on:
|
|||||||
- .github/workflows/labels.yml
|
- .github/workflows/labels.yml
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v5
|
||||||
- uses: crazy-max/ghaction-github-labeler@v3
|
- uses: crazy-max/ghaction-github-labeler@v5
|
||||||
with:
|
with:
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
|
|||||||
21
.github/workflows/markdown-skip.yml
vendored
Normal file
21
.github/workflows/markdown-skip.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Markdown
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- .github/workflows/markdown.yml
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- .github/workflows/markdown.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
markdown:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: No trigger path triggered for required markdown workflow.
|
||||||
|
run: exit 0
|
||||||
47
.github/workflows/markdown.yml
vendored
Normal file
47
.github/workflows/markdown.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Markdown
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- "**.md"
|
||||||
|
- .github/workflows/markdown.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "**.md"
|
||||||
|
- .github/workflows/markdown.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
markdown:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: DavidAnson/markdownlint-cli2-action@v20
|
||||||
|
with:
|
||||||
|
globs: "**.md"
|
||||||
|
config: .markdownlint-cli2.jsonc
|
||||||
|
|
||||||
|
- uses: reviewdog/action-misspell@v1
|
||||||
|
with:
|
||||||
|
locale: "US"
|
||||||
|
level: error
|
||||||
|
pattern: |
|
||||||
|
*.md
|
||||||
|
|
||||||
|
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||||
|
with:
|
||||||
|
use-quiet-mode: yes
|
||||||
|
config-file: .github/workflows/configs/mlc-config.json
|
||||||
|
|
||||||
|
- uses: peter-evans/dockerhub-description@v4
|
||||||
|
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||||
|
with:
|
||||||
|
username: qmcgaw
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
repository: qmcgaw/gluetun
|
||||||
|
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
||||||
|
readme-filepath: README.md
|
||||||
15
.github/workflows/misspell.yml
vendored
15
.github/workflows/misspell.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Misspells
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
jobs:
|
|
||||||
misspell:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.4.0
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
|
||||||
with:
|
|
||||||
locale: "US"
|
|
||||||
level: error
|
|
||||||
22
.github/workflows/opened-issue.yml
vendored
Normal file
22
.github/workflows/opened-issue.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Opened issue
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
@qdm12 is more or less the only maintainer of this project and works on it in his free time.
|
||||||
|
Please:
|
||||||
|
- **do not** ask for updates, be patient
|
||||||
|
- :+1: the issue to show your support instead of commenting
|
||||||
|
@qdm12 usually checks issues at least once a week, if this is a new urgent bug,
|
||||||
|
[revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md)
|
||||||
145
.golangci.yml
145
.golangci.yml
@@ -1,58 +1,94 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
misspell:
|
|
||||||
locale: US
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|
||||||
issues:
|
|
||||||
exclude-rules:
|
|
||||||
- path: _test\.go
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
- maligned
|
|
||||||
- goerr113
|
|
||||||
- path: internal/server/
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
- path: internal/configuration/
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
- path: internal/constants/
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
- text: "exported: exported var Err*"
|
|
||||||
linters:
|
|
||||||
- revive
|
|
||||||
- text: "mnd: Magic number: 0644*"
|
|
||||||
linters:
|
|
||||||
- gomnd
|
|
||||||
- text: "mnd: Magic number: 0400*"
|
|
||||||
linters:
|
|
||||||
- gomnd
|
|
||||||
- text: "variable 'mssFix' is only used in the if-statement*"
|
|
||||||
path: "openvpnconf.go"
|
|
||||||
linters:
|
|
||||||
- ifshort
|
|
||||||
- text: "variable 'auth' is only used in the if-statement*"
|
|
||||||
path: "openvpnconf.go"
|
|
||||||
linters:
|
|
||||||
- ifshort
|
|
||||||
linters:
|
linters:
|
||||||
|
settings:
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
goconst:
|
||||||
|
ignore-string-values:
|
||||||
|
# commonly used settings strings
|
||||||
|
- "^disabled$"
|
||||||
|
# Firewall and routing strings
|
||||||
|
- "^(ACCEPT|DROP)$"
|
||||||
|
- "^--delete$"
|
||||||
|
- "^all$"
|
||||||
|
- "^(tcp|udp)$"
|
||||||
|
# Server route strings
|
||||||
|
- "^/status$"
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- containedctx
|
||||||
|
- dupl
|
||||||
|
- err113
|
||||||
|
- maintidx
|
||||||
|
path: _test\.go
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
path: internal\/server\/.+\.go
|
||||||
|
- linters:
|
||||||
|
- ireturn
|
||||||
|
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
||||||
|
- linters:
|
||||||
|
- ireturn
|
||||||
|
path: internal\/openvpn\/pkcs8\/descbc\.go
|
||||||
|
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
path: internal\/provider\/(common|utils)\/.+\.go
|
||||||
|
text: "var-naming: avoid (bad|meaningless) package names"
|
||||||
|
- linters:
|
||||||
|
- lll
|
||||||
|
source: "^// https://.+$"
|
||||||
|
- linters:
|
||||||
|
- err113
|
||||||
|
- mnd
|
||||||
|
path: ci\/.+\.go
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
enable:
|
enable:
|
||||||
# - cyclop
|
# - cyclop
|
||||||
# - errorlint
|
# - errorlint
|
||||||
# - ireturn
|
- asasalint
|
||||||
# - varnamelen
|
|
||||||
# - wrapcheck
|
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bidichk
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- copyloopvar
|
||||||
|
- decorder
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
|
- dupword
|
||||||
- durationcheck
|
- durationcheck
|
||||||
|
- err113
|
||||||
|
- errchkjson
|
||||||
- errname
|
- errname
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- fatcontext
|
||||||
- forcetypeassert
|
- forcetypeassert
|
||||||
- gci
|
- gocheckcompilerdirectives
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gocognit
|
- gocognit
|
||||||
@@ -60,41 +96,44 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godot
|
- godot
|
||||||
- goerr113
|
|
||||||
- goheader
|
- goheader
|
||||||
- goimports
|
|
||||||
- gomnd
|
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- ifshort
|
- gosmopolitan
|
||||||
|
- grouper
|
||||||
- importas
|
- importas
|
||||||
|
- interfacebloat
|
||||||
|
- intrange
|
||||||
|
- ireturn
|
||||||
- lll
|
- lll
|
||||||
|
- maintidx
|
||||||
- makezero
|
- makezero
|
||||||
|
- mirror
|
||||||
- misspell
|
- misspell
|
||||||
|
- mnd
|
||||||
|
- musttag
|
||||||
- nakedret
|
- nakedret
|
||||||
- nestif
|
- nestif
|
||||||
- nilerr
|
- nilerr
|
||||||
- nilnil
|
- nilnil
|
||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
|
- nosprintfhostport
|
||||||
|
- paralleltest
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- predeclared
|
|
||||||
- promlinter
|
- promlinter
|
||||||
|
- reassign
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- tenv
|
- tagalign
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
|
- usestdlibvars
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
|
- zerologlint
|
||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- .devcontainer
|
|
||||||
- .github
|
|
||||||
- doc
|
|
||||||
|
|||||||
9
.markdownlint-cli2.jsonc
Normal file
9
.markdownlint-cli2.jsonc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
},
|
||||||
|
"ignores": [
|
||||||
|
".github/pull_request_template.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
// This list should be kept to the strict minimum
|
||||||
|
// to develop this project.
|
||||||
|
"recommendations": [
|
||||||
|
"golang.go",
|
||||||
|
"davidanson.vscode-markdownlint",
|
||||||
|
],
|
||||||
|
}
|
||||||
29
.vscode/settings.json
vendored
Normal file
29
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
// The settings should be kept to the strict minimum
|
||||||
|
// to develop this project.
|
||||||
|
"files.eol": "\n",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"go.buildTags": "linux",
|
||||||
|
"go.toolsEnvVars": {
|
||||||
|
"CGO_ENABLED": "0"
|
||||||
|
},
|
||||||
|
"go.testEnvVars": {
|
||||||
|
"CGO_ENABLED": "1"
|
||||||
|
},
|
||||||
|
"go.testFlags": [
|
||||||
|
"-v",
|
||||||
|
"-race"
|
||||||
|
],
|
||||||
|
"go.testTimeout": "10s",
|
||||||
|
"go.coverOnSingleTest": true,
|
||||||
|
"go.coverOnSingleTestFile": true,
|
||||||
|
"go.coverOnTestPackage": true,
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"[go]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"go.lintTool": "golangci-lint",
|
||||||
|
"go.lintOnSave": "package"
|
||||||
|
}
|
||||||
51
.vscode/tasks.json
vendored
Normal file
51
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Update a VPN provider servers data",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"./cmd/gluetun/main.go",
|
||||||
|
"update",
|
||||||
|
"${input:updateMode}",
|
||||||
|
"-providers",
|
||||||
|
"${input:provider}"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Add a Gluetun Github Git remote",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "git",
|
||||||
|
"args": [
|
||||||
|
"remote",
|
||||||
|
"add",
|
||||||
|
"${input:githubRemoteUsername}",
|
||||||
|
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "provider",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Please enter a provider (or comma separated list of providers)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "updateMode",
|
||||||
|
"type": "pickString",
|
||||||
|
"description": "Update mode to use",
|
||||||
|
"options": [
|
||||||
|
"-maintainer",
|
||||||
|
"-enduser"
|
||||||
|
],
|
||||||
|
"default": "-maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "githubRemoteUsername",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Please enter a Github username",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
185
Dockerfile
185
Dockerfile
@@ -1,18 +1,22 @@
|
|||||||
ARG ALPINE_VERSION=3.15
|
ARG ALPINE_VERSION=3.22
|
||||||
ARG GO_ALPINE_VERSION=3.15
|
ARG GO_ALPINE_VERSION=3.22
|
||||||
ARG GO_VERSION=1.17
|
ARG GO_VERSION=1.25
|
||||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
||||||
ARG GOLANGCI_LINT_VERSION=v1.43.0
|
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
||||||
|
ARG MOCKGEN_VERSION=v1.6.0
|
||||||
ARG BUILDPLATFORM=linux/amd64
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||||
|
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||||
RUN apk --update add git g++
|
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
|
||||||
|
RUN apk --update add git g++ findutils
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||||
|
COPY --from=mockgen /bin /go/bin/mockgen
|
||||||
WORKDIR /tmp/gobuild
|
WORKDIR /tmp/gobuild
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@@ -28,16 +32,19 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
|||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||||
COPY .golangci.yml ./
|
COPY .golangci.yml ./
|
||||||
RUN golangci-lint run --timeout=10m
|
RUN golangci-lint run
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS tidy
|
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||||
RUN git init && \
|
RUN git init && \
|
||||||
git config user.email ci@localhost && \
|
git config user.email ci@localhost && \
|
||||||
git config user.name ci && \
|
git config user.name ci && \
|
||||||
git add -A && git commit -m ci && \
|
git config core.fileMode false && \
|
||||||
sed -i '/\/\/ indirect/d' go.mod && \
|
git add -A && \
|
||||||
go mod tidy && \
|
git commit -m "snapshot" && \
|
||||||
git diff --exit-code -- go.mod
|
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
|
||||||
|
go generate -run "mockgen" ./... && \
|
||||||
|
git diff --exit-code && \
|
||||||
|
rm -rf .git/
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS build
|
FROM --platform=${BUILDPLATFORM} base AS build
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -66,58 +73,88 @@ LABEL \
|
|||||||
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
||||||
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
|
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
|
||||||
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
|
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
|
||||||
ENV VPNSP=pia \
|
ENV VPN_SERVICE_PROVIDER=pia \
|
||||||
VPN_TYPE=openvpn \
|
VPN_TYPE=openvpn \
|
||||||
|
# Common VPN options
|
||||||
|
VPN_INTERFACE=tun0 \
|
||||||
# OpenVPN
|
# OpenVPN
|
||||||
|
OPENVPN_ENDPOINT_IP= \
|
||||||
|
OPENVPN_ENDPOINT_PORT= \
|
||||||
OPENVPN_PROTOCOL=udp \
|
OPENVPN_PROTOCOL=udp \
|
||||||
OPENVPN_USER= \
|
OPENVPN_USER= \
|
||||||
OPENVPN_PASSWORD= \
|
OPENVPN_PASSWORD= \
|
||||||
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
|
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
|
||||||
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
|
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
|
||||||
OPENVPN_VERSION=2.5 \
|
OPENVPN_VERSION=2.6 \
|
||||||
OPENVPN_VERBOSITY=1 \
|
OPENVPN_VERBOSITY=1 \
|
||||||
OPENVPN_FLAGS= \
|
OPENVPN_FLAGS= \
|
||||||
OPENVPN_CIPHER= \
|
OPENVPN_CIPHERS= \
|
||||||
OPENVPN_AUTH= \
|
OPENVPN_AUTH= \
|
||||||
OPENVPN_ROOT=yes \
|
OPENVPN_PROCESS_USER=root \
|
||||||
OPENVPN_TARGET_IP= \
|
OPENVPN_MSSFIX= \
|
||||||
OPENVPN_IPV6=off \
|
|
||||||
OPENVPN_CUSTOM_CONFIG= \
|
OPENVPN_CUSTOM_CONFIG= \
|
||||||
OPENVPN_INTERFACE=tun0 \
|
|
||||||
OPENVPN_PORT= \
|
|
||||||
# Wireguard
|
# Wireguard
|
||||||
WIREGUARD_PRIVATE_KEY= \
|
|
||||||
WIREGUARD_PRESHARED_KEY= \
|
|
||||||
WIREGUARD_PUBLIC_KEY= \
|
|
||||||
WIREGUARD_ADDRESS= \
|
|
||||||
WIREGUARD_ENDPOINT_IP= \
|
WIREGUARD_ENDPOINT_IP= \
|
||||||
WIREGUARD_ENDPOINT_PORT= \
|
WIREGUARD_ENDPOINT_PORT= \
|
||||||
WIREGUARD_INTERFACE=wg0 \
|
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
|
||||||
|
WIREGUARD_PRIVATE_KEY= \
|
||||||
|
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
|
||||||
|
WIREGUARD_PRESHARED_KEY= \
|
||||||
|
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
|
||||||
|
WIREGUARD_PUBLIC_KEY= \
|
||||||
|
WIREGUARD_ALLOWED_IPS= \
|
||||||
|
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||||
|
WIREGUARD_ADDRESSES= \
|
||||||
|
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||||
|
WIREGUARD_MTU=1320 \
|
||||||
|
WIREGUARD_IMPLEMENTATION=auto \
|
||||||
# VPN server filtering
|
# VPN server filtering
|
||||||
REGION= \
|
SERVER_REGIONS= \
|
||||||
COUNTRY= \
|
SERVER_COUNTRIES= \
|
||||||
CITY= \
|
SERVER_CITIES= \
|
||||||
SERVER_HOSTNAME= \
|
SERVER_HOSTNAMES= \
|
||||||
|
SERVER_CATEGORIES= \
|
||||||
# # Mullvad only:
|
# # Mullvad only:
|
||||||
ISP= \
|
ISP= \
|
||||||
OWNED=no \
|
OWNED_ONLY=no \
|
||||||
# # Private Internet Access only:
|
# # Private Internet Access only:
|
||||||
PIA_ENCRYPTION= \
|
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
||||||
PORT_FORWARDING=off \
|
VPN_PORT_FORWARDING=off \
|
||||||
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
|
||||||
|
VPN_PORT_FORWARDING_PROVIDER= \
|
||||||
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
|
VPN_PORT_FORWARDING_USERNAME= \
|
||||||
|
VPN_PORT_FORWARDING_PASSWORD= \
|
||||||
|
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||||
|
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||||
# # Cyberghost only:
|
# # Cyberghost only:
|
||||||
|
OPENVPN_CERT= \
|
||||||
|
OPENVPN_KEY= \
|
||||||
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
||||||
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
||||||
|
# # VPNSecure only:
|
||||||
|
OPENVPN_ENCRYPTED_KEY= \
|
||||||
|
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
|
||||||
|
OPENVPN_KEY_PASSPHRASE= \
|
||||||
|
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
|
||||||
# # Nordvpn only:
|
# # Nordvpn only:
|
||||||
SERVER_NUMBER= \
|
SERVER_NUMBER= \
|
||||||
# # PIA and ProtonVPN only:
|
# # PIA only:
|
||||||
SERVER_NAME= \
|
SERVER_NAMES= \
|
||||||
# # ProtonVPN only:
|
# # VPNUnlimited and ProtonVPN only:
|
||||||
|
STREAM_ONLY= \
|
||||||
FREE_ONLY= \
|
FREE_ONLY= \
|
||||||
|
# # ProtonVPN only:
|
||||||
|
SECURE_CORE_ONLY= \
|
||||||
|
TOR_ONLY= \
|
||||||
# # Surfshark only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
|
# # VPN Secure only:
|
||||||
|
PREMIUM_ONLY= \
|
||||||
|
# # PIA and ProtonVPN only:
|
||||||
|
PORT_FORWARD_ONLY= \
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL=on \
|
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
FIREWALL_INPUT_PORTS= \
|
FIREWALL_INPUT_PORTS= \
|
||||||
FIREWALL_OUTBOUND_SUBNETS= \
|
FIREWALL_OUTBOUND_SUBNETS= \
|
||||||
@@ -126,29 +163,30 @@ ENV VPNSP=pia \
|
|||||||
LOG_LEVEL=info \
|
LOG_LEVEL=info \
|
||||||
# Health
|
# Health
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_ADDRESS_TO_PING=github.com \
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
HEALTH_VPN_DURATION_INITIAL=6s \
|
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
||||||
HEALTH_VPN_DURATION_ADDITION=5s \
|
HEALTH_RESTART_VPN=on \
|
||||||
# DNS over TLS
|
# DNS
|
||||||
DOT=on \
|
DNS_SERVER=on \
|
||||||
DOT_PROVIDERS=cloudflare \
|
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||||
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||||
DOT_VERBOSITY=1 \
|
DNS_BLOCK_IPS= \
|
||||||
DOT_VERBOSITY_DETAILS=0 \
|
DNS_BLOCK_IP_PREFIXES= \
|
||||||
DOT_VALIDATION_LOGLEVEL=0 \
|
DNS_CACHING=on \
|
||||||
DOT_CACHING=on \
|
DNS_UPSTREAM_IPV6=off \
|
||||||
DOT_IPV6=off \
|
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
BLOCK_SURVEILLANCE=off \
|
BLOCK_SURVEILLANCE=off \
|
||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
UNBLOCK= \
|
DNS_UNBLOCK_HOSTNAMES= \
|
||||||
|
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||||
DNS_UPDATE_PERIOD=24h \
|
DNS_UPDATE_PERIOD=24h \
|
||||||
DNS_PLAINTEXT_ADDRESS=127.0.0.1 \
|
DNS_ADDRESS=127.0.0.1 \
|
||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
# HTTP proxy
|
# HTTP proxy
|
||||||
HTTPPROXY= \
|
HTTPPROXY= \
|
||||||
HTTPPROXY_LOG=off \
|
HTTPPROXY_LOG=off \
|
||||||
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
||||||
|
HTTPPROXY_STEALTH=off \
|
||||||
HTTPPROXY_USER= \
|
HTTPPROXY_USER= \
|
||||||
HTTPPROXY_PASSWORD= \
|
HTTPPROXY_PASSWORD= \
|
||||||
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||||
@@ -160,30 +198,45 @@ ENV VPNSP=pia \
|
|||||||
SHADOWSOCKS_PASSWORD= \
|
SHADOWSOCKS_PASSWORD= \
|
||||||
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||||
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
||||||
|
# Control server
|
||||||
|
HTTP_CONTROL_SERVER_LOG=on \
|
||||||
|
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||||
|
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||||
|
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||||
# Server data updater
|
# Server data updater
|
||||||
UPDATER_PERIOD=0 \
|
UPDATER_PERIOD=0 \
|
||||||
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
|
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||||
|
UPDATER_PROTONVPN_USERNAME= \
|
||||||
|
UPDATER_PROTONVPN_PASSWORD= \
|
||||||
# Public IP
|
# Public IP
|
||||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
PUBLICIP_PERIOD=12h \
|
PUBLICIP_ENABLED=on \
|
||||||
|
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
||||||
|
PUBLICIP_API_TOKEN= \
|
||||||
|
# Storage
|
||||||
|
STORAGE_FILEPATH=/gluetun/servers.json \
|
||||||
|
# Pprof
|
||||||
|
PPROF_ENABLED=no \
|
||||||
|
PPROF_BLOCK_PROFILE_RATE=0 \
|
||||||
|
PPROF_MUTEX_PROFILE_RATE=0 \
|
||||||
|
PPROF_HTTP_SERVER_ADDRESS=":6060" \
|
||||||
# Extras
|
# Extras
|
||||||
VERSION_INFORMATION=on \
|
VERSION_INFORMATION=on \
|
||||||
TZ= \
|
TZ= \
|
||||||
PUID= \
|
PUID=1000 \
|
||||||
PGID=
|
PGID=1000
|
||||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
|
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
RUN apk add --no-cache --update -l apk-tools && \
|
RUN apk add --no-cache --update -l wget && \
|
||||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-r0 && \
|
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
|
||||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
|
|
||||||
apk del openvpn && \
|
|
||||||
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
|
|
||||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
|
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
|
||||||
# Fix vulnerability issue
|
apk del openvpn && \
|
||||||
apk add --no-cache --update busybox && \
|
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
||||||
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
|
||||||
|
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||||
deluser openvpn && \
|
deluser openvpn && \
|
||||||
deluser unbound && \
|
|
||||||
mkdir /gluetun
|
mkdir /gluetun
|
||||||
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
||||||
|
|||||||
255
README.md
255
README.md
@@ -1,127 +1,128 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
|
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||||
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
|
|
||||||
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
|

|
||||||
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
|
|
||||||
|
[](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
|
||||||
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
|
|
||||||
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||

|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|

|
||||||
|

|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
[](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|

|
||||||
|

|
||||||

|
|
||||||

|
[](https://hub.docker.com/r/qmcgaw/gluetun/tags)
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
|
|
||||||

|
[](https://github.com/qdm12/gluetun/commits/master)
|
||||||

|
[](https://github.com/qdm12/gluetun/graphs/contributors)
|
||||||
|
[](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags)
|
[](https://github.com/qdm12/gluetun/issues)
|
||||||
|
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
[](https://github.com/qdm12/gluetun/commits/master)
|
|
||||||
[](https://github.com/qdm12/gluetun/graphs/contributors)
|

|
||||||
[](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
|

|
||||||
[](https://github.com/qdm12/gluetun/issues)
|

|
||||||
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
|
||||||
|

|
||||||
[](https://github.com/qdm12/gluetun)
|
|
||||||

|
## Quick links
|
||||||

|
|
||||||

|
- [Setup](#setup)
|
||||||
|
- [Features](#features)
|
||||||

|
- Problem?
|
||||||
|
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
|
||||||
## Quick links
|
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||||
|
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
||||||
- [Setup](#Setup)
|
- Suggestion?
|
||||||
- [Features](#Features)
|
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
||||||
- Problem?
|
- Happy?
|
||||||
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
|
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||||
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||||
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
||||||
- Suggestion?
|
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
|
||||||
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
- Video:
|
||||||
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
|
||||||
- Happy?
|
[](https://youtu.be/0F6I03LQcI4)
|
||||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
|
||||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
- [Substack Console interview](https://console.substack.com/p/console-72)
|
||||||
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
|
||||||
- Video:
|
## Features
|
||||||
|
|
||||||
[](https://youtu.be/0F6I03LQcI4)
|
- Based on Alpine 3.22 for a small Docker image of 41.1MB
|
||||||
|
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- [Substack Console interview](https://console.substack.com/p/console-72)
|
- Supports OpenVPN for all providers listed
|
||||||
|
- Supports Wireguard both kernelspace and userspace
|
||||||
## Features
|
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||||
|
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- Based on Alpine 3.15 for a small Docker image of 29MB
|
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||||
- Supports OpenVPN for all providers listed
|
- DNS over TLS baked in with service provider(s) of your choice
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
||||||
- For **Mullvad**, **Ivpn** and **Windscribe**
|
- Choose the vpn network protocol, `udp` or `tcp`
|
||||||
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||||
- DNS over TLS baked in with service provider(s) of your choice
|
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||||
- Choose the vpn network protocol, `udp` or `tcp`
|
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
||||||
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||||
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
|
|
||||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
|
## Setup
|
||||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
|
||||||
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
|
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
||||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
|
||||||
- Unbound subprogram drops root privileges once launched
|
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
|
||||||
|
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
|
||||||
## Setup
|
|
||||||
|
Here's a docker-compose.yml for the laziest:
|
||||||
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
|
||||||
|
```yml
|
||||||
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
|
---
|
||||||
|
services:
|
||||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
|
gluetun:
|
||||||
|
image: qmcgaw/gluetun
|
||||||
Here's a docker-compose.yml for the laziest:
|
# container_name: gluetun
|
||||||
|
# line above must be uncommented to allow external containers to connect.
|
||||||
```yml
|
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
|
||||||
version: "3"
|
cap_add:
|
||||||
services:
|
- NET_ADMIN
|
||||||
gluetun:
|
devices:
|
||||||
image: qmcgaw/gluetun
|
- /dev/net/tun:/dev/net/tun
|
||||||
# container_name: gluetun
|
ports:
|
||||||
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
|
- 8888:8888/tcp # HTTP proxy
|
||||||
cap_add:
|
- 8388:8388/tcp # Shadowsocks
|
||||||
- NET_ADMIN
|
- 8388:8388/udp # Shadowsocks
|
||||||
ports:
|
volumes:
|
||||||
- 8888:8888/tcp # HTTP proxy
|
- /yourpath:/gluetun
|
||||||
- 8388:8388/tcp # Shadowsocks
|
environment:
|
||||||
- 8388:8388/udp # Shadowsocks
|
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
||||||
volumes:
|
- VPN_SERVICE_PROVIDER=ivpn
|
||||||
- /yourpath:/gluetun
|
- VPN_TYPE=openvpn
|
||||||
environment:
|
# OpenVPN:
|
||||||
# See https://github.com/qdm12/gluetun/wiki
|
- OPENVPN_USER=
|
||||||
- VPNSP=ivpn
|
- OPENVPN_PASSWORD=
|
||||||
- VPN_TYPE=openvpn
|
# Wireguard:
|
||||||
# OpenVPN:
|
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
||||||
- OPENVPN_USER=
|
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||||
- OPENVPN_PASSWORD=
|
# Timezone for accurate log times
|
||||||
# Wireguard:
|
- TZ=
|
||||||
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
# Server list updater
|
||||||
# - WIREGUARD_ADDRESS=10.64.222.21/32
|
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||||
# Timezone for accurate log times
|
- UPDATER_PERIOD=
|
||||||
- TZ=
|
```
|
||||||
```
|
|
||||||
|
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||||
## License
|
|
||||||
|
## License
|
||||||
[](https://github.com/qdm12/gluetun/master/LICENSE)
|
|
||||||
|
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
||||||
|
|||||||
33
ci/cmd/main.go
Normal file
33
ci/cmd/main.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/ci/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: " + os.Args[0] + " <command>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "mullvad":
|
||||||
|
err = internal.MullvadTest(ctx)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
||||||
|
}
|
||||||
|
stop()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("❌", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("✅ Test completed successfully.")
|
||||||
|
}
|
||||||
36
ci/go.mod
Normal file
36
ci/go.mod
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module github.com/qdm12/gluetun/ci
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/docker/docker v28.5.1+incompatible
|
||||||
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
|
)
|
||||||
97
ci/go.sum
Normal file
97
ci/go.sum
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||||
|
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||||
|
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||||
|
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
193
ci/internal/mullvad.go
Normal file
193
ci/internal/mullvad.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MullvadTest(ctx context.Context) error {
|
||||||
|
secrets, err := readSecrets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = 15 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating Docker client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
config := &container.Config{
|
||||||
|
Image: "qmcgaw/gluetun",
|
||||||
|
StopTimeout: ptrTo(3),
|
||||||
|
Env: []string{
|
||||||
|
"VPN_SERVICE_PROVIDER=mullvad",
|
||||||
|
"VPN_TYPE=wireguard",
|
||||||
|
"LOG_LEVEL=debug",
|
||||||
|
"SERVER_COUNTRIES=USA",
|
||||||
|
"WIREGUARD_PRIVATE_KEY=" + secrets.mullvadWireguardPrivateKey,
|
||||||
|
"WIREGUARD_ADDRESSES=" + secrets.mullvadWireguardAddress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
AutoRemove: true,
|
||||||
|
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
||||||
|
}
|
||||||
|
networkConfig := (*network.NetworkingConfig)(nil)
|
||||||
|
platform := (*v1.Platform)(nil)
|
||||||
|
const containerName = "" // auto-generated name
|
||||||
|
|
||||||
|
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating container: %w", err)
|
||||||
|
}
|
||||||
|
for _, warning := range response.Warnings {
|
||||||
|
fmt.Println("Warning during container creation:", warning)
|
||||||
|
}
|
||||||
|
containerID := response.ID
|
||||||
|
defer stopContainer(client, containerID)
|
||||||
|
|
||||||
|
beforeStartTime := time.Now()
|
||||||
|
|
||||||
|
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitForLogLine(ctx, client, containerID, beforeStartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrTo[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
type secrets struct {
|
||||||
|
mullvadWireguardPrivateKey string
|
||||||
|
mullvadWireguardAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSecrets(ctx context.Context) (secrets, error) {
|
||||||
|
expectedSecrets := [...]string{
|
||||||
|
"Mullvad Wireguard private key",
|
||||||
|
"Mullvad Wireguard address",
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
lines := make([]string, 0, len(expectedSecrets))
|
||||||
|
|
||||||
|
for i := range expectedSecrets {
|
||||||
|
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return secrets{}, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return secrets{}, fmt.Errorf("reading secrets from stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lines) < len(expectedSecrets) {
|
||||||
|
return secrets{}, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
||||||
|
len(expectedSecrets), len(lines))
|
||||||
|
}
|
||||||
|
for i, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
return secrets{}, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets{
|
||||||
|
mullvadWireguardPrivateKey: lines[0],
|
||||||
|
mullvadWireguardAddress: lines[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopContainer(client *client.Client, containerID string) {
|
||||||
|
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
||||||
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||||
|
defer stopCancel()
|
||||||
|
|
||||||
|
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to stop container:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
||||||
|
|
||||||
|
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
||||||
|
beforeStartTime time.Time,
|
||||||
|
) error {
|
||||||
|
logOptions := container.LogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
Follow: true,
|
||||||
|
Since: beforeStartTime.Format(time.RFC3339Nano),
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting container logs: %w", err)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
var linesSeen []string
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
if scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) > 8 { // remove Docker log prefix
|
||||||
|
line = line[8:]
|
||||||
|
}
|
||||||
|
linesSeen = append(linesSeen, line)
|
||||||
|
if successRegexp.MatchString(line) {
|
||||||
|
fmt.Println("✅ Success line logged")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := scanner.Err()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
logSeenLines(linesSeen)
|
||||||
|
return fmt.Errorf("reading log stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The scanner is either done or cannot read because of EOF
|
||||||
|
fmt.Println("The log scanner stopped")
|
||||||
|
logSeenLines(linesSeen)
|
||||||
|
|
||||||
|
// Check if the container is still running
|
||||||
|
inspect, err := client.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspecting container: %w", err)
|
||||||
|
}
|
||||||
|
if !inspect.State.Running {
|
||||||
|
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func logSeenLines(lines []string) {
|
||||||
|
fmt.Println("Logs seen so far:")
|
||||||
|
for _, line := range lines {
|
||||||
|
fmt.Println(" " + line)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -13,13 +15,11 @@ import (
|
|||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
_ "github.com/breml/rootcerts"
|
_ "github.com/breml/rootcerts"
|
||||||
"github.com/qdm12/dns/pkg/unbound"
|
|
||||||
"github.com/qdm12/gluetun/internal/alpine"
|
"github.com/qdm12/gluetun/internal/alpine"
|
||||||
"github.com/qdm12/gluetun/internal/cli"
|
"github.com/qdm12/gluetun/internal/cli"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
"github.com/qdm12/gluetun/internal/command"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/env"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/dns"
|
"github.com/qdm12/gluetun/internal/dns"
|
||||||
@@ -29,23 +29,28 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/qdm12/gluetun/internal/netlink"
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
"github.com/qdm12/gluetun/internal/openvpn"
|
"github.com/qdm12/gluetun/internal/openvpn"
|
||||||
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||||
"github.com/qdm12/gluetun/internal/portforward"
|
"github.com/qdm12/gluetun/internal/portforward"
|
||||||
|
"github.com/qdm12/gluetun/internal/pprof"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
"github.com/qdm12/gluetun/internal/publicip"
|
"github.com/qdm12/gluetun/internal/publicip"
|
||||||
"github.com/qdm12/gluetun/internal/routing"
|
"github.com/qdm12/gluetun/internal/routing"
|
||||||
"github.com/qdm12/gluetun/internal/server"
|
"github.com/qdm12/gluetun/internal/server"
|
||||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
"github.com/qdm12/gluetun/internal/tun"
|
"github.com/qdm12/gluetun/internal/tun"
|
||||||
"github.com/qdm12/gluetun/internal/updater"
|
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
"github.com/qdm12/gluetun/internal/vpn"
|
"github.com/qdm12/gluetun/internal/vpn"
|
||||||
"github.com/qdm12/golibs/command"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/golibs/logging"
|
"github.com/qdm12/gosettings/reader/sources/env"
|
||||||
"github.com/qdm12/goshutdown"
|
"github.com/qdm12/goshutdown"
|
||||||
"github.com/qdm12/goshutdown/goroutine"
|
"github.com/qdm12/goshutdown/goroutine"
|
||||||
"github.com/qdm12/goshutdown/group"
|
"github.com/qdm12/goshutdown/group"
|
||||||
"github.com/qdm12/goshutdown/order"
|
"github.com/qdm12/goshutdown/order"
|
||||||
"github.com/qdm12/gosplash"
|
"github.com/qdm12/gosplash"
|
||||||
"github.com/qdm12/updated/pkg/dnscrypto"
|
"github.com/qdm12/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
@@ -55,11 +60,6 @@ var (
|
|||||||
created = "an unknown date"
|
created = "an unknown date"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
errSetupRouting = errors.New("cannot setup routing")
|
|
||||||
errCreateUser = errors.New("cannot create user")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
buildInfo := models.BuildInformation{
|
buildInfo := models.BuildInformation{
|
||||||
Version: version,
|
Version: version,
|
||||||
@@ -68,37 +68,45 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
background := context.Background()
|
background := context.Background()
|
||||||
signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
signalCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
||||||
ctx, cancel := context.WithCancel(background)
|
ctx, cancel := context.WithCancel(background)
|
||||||
|
|
||||||
logger := logging.New(logging.Settings{
|
logger := log.New(log.SetLevel(log.LevelInfo))
|
||||||
Level: logging.LevelInfo,
|
|
||||||
})
|
|
||||||
|
|
||||||
args := os.Args
|
args := os.Args
|
||||||
tun := tun.New()
|
tun := tun.New()
|
||||||
netLinker := netlink.New()
|
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||||
|
netLinker := netlink.New(netLinkDebugLogger)
|
||||||
cli := cli.New()
|
cli := cli.New()
|
||||||
cmder := command.NewCmder()
|
cmder := command.New()
|
||||||
|
|
||||||
envReader := env.New(logger)
|
reader := reader.New(reader.Settings{
|
||||||
filesReader := files.New()
|
Sources: []reader.Source{
|
||||||
secretsReader := secrets.New()
|
secrets.New(logger),
|
||||||
muxReader := mux.New(envReader, filesReader, secretsReader)
|
files.New(logger),
|
||||||
|
env.New(env.Settings{}),
|
||||||
|
},
|
||||||
|
HandleDeprecatedKey: func(source, deprecatedKey, currentKey string) {
|
||||||
|
logger.Warn("You are using the old " + source + " " + deprecatedKey +
|
||||||
|
", please consider changing it to " + currentKey)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
errorCh := make(chan error)
|
errorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli)
|
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Wait for OS signal or run error
|
||||||
|
var err error
|
||||||
select {
|
select {
|
||||||
case <-signalCtx.Done():
|
case receivedSignal := <-signalCh:
|
||||||
stop()
|
signal.Stop(signalCh)
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
logger.Warn("Caught OS signal, shutting down")
|
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||||
cancel()
|
cancel()
|
||||||
case err := <-errorCh:
|
case err = <-errorCh:
|
||||||
stop()
|
|
||||||
close(errorCh)
|
close(errorCh)
|
||||||
if err == nil { // expected exit such as healthcheck
|
if err == nil { // expected exit such as healthcheck
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@@ -107,48 +115,56 @@ func main() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown timed sequence, and force exit on second OS signal
|
||||||
const shutdownGracePeriod = 5 * time.Second
|
const shutdownGracePeriod = 5 * time.Second
|
||||||
timer := time.NewTimer(shutdownGracePeriod)
|
timer := time.NewTimer(shutdownGracePeriod)
|
||||||
select {
|
select {
|
||||||
case <-errorCh:
|
case shutdownErr := <-errorCh:
|
||||||
if !timer.Stop() {
|
timer.Stop()
|
||||||
<-timer.C
|
if shutdownErr != nil {
|
||||||
|
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Shutdown successful")
|
logger.Info("Shutdown successful")
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
logger.Warn("Shutdown timed out")
|
logger.Warn("Shutdown timed out")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var errCommandUnknown = errors.New("command is unknown")
|
||||||
errCommandUnknown = errors.New("command is unknown")
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gocognit,gocyclo
|
//nolint:gocognit,gocyclo,maintidx
|
||||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||||
args []string, logger logging.ParentLogger, source sources.Source,
|
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||||
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
|
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||||
cli cli.CLIer) error {
|
cli clier,
|
||||||
|
) error {
|
||||||
if len(args) > 1 { // cli operation
|
if len(args) > 1 { // cli operation
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
return cli.HealthCheck(ctx, source, logger)
|
return cli.HealthCheck(ctx, reader, logger)
|
||||||
case "clientkey":
|
case "clientkey":
|
||||||
return cli.ClientKey(args[2:])
|
return cli.ClientKey(args[2:])
|
||||||
case "openvpnconfig":
|
case "openvpnconfig":
|
||||||
return cli.OpenvpnConfig(logger, source)
|
return cli.OpenvpnConfig(logger, reader, netLinker)
|
||||||
case "update":
|
case "update":
|
||||||
return cli.Update(ctx, args[2:], logger)
|
return cli.Update(ctx, args[2:], logger)
|
||||||
case "format-servers":
|
case "format-servers":
|
||||||
return cli.FormatServers(args[2:])
|
return cli.FormatServers(args[2:])
|
||||||
|
case "genkey":
|
||||||
|
return cli.GenKey(args[2:])
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
|
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -158,8 +174,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
Emails: []string{"quentin.mcgaw@gmail.com"},
|
Emails: []string{"quentin.mcgaw@gmail.com"},
|
||||||
Version: buildInfo.Version,
|
Version: buildInfo.Version,
|
||||||
Commit: buildInfo.Commit,
|
Commit: buildInfo.Commit,
|
||||||
BuildDate: buildInfo.Created,
|
Created: buildInfo.Created,
|
||||||
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
|
Announcement: "All control server routes are now private by default",
|
||||||
AnnounceExp: announcementExp,
|
AnnounceExp: announcementExp,
|
||||||
// Sponsor information
|
// Sponsor information
|
||||||
PaypalUser: "qmcgaw",
|
PaypalUser: "qmcgaw",
|
||||||
@@ -169,91 +185,30 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
fmt.Println(line)
|
fmt.Println(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
allSettings, err := source.Read()
|
var allSettings settings.Settings
|
||||||
|
err = allSettings.Read(reader, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
allSettings.SetDefaults()
|
||||||
|
|
||||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
// Note: no need to validate minimal settings for the firewall:
|
||||||
storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "})
|
// - global log level is parsed below
|
||||||
storage, err := storage.New(storageLogger, constants.ServersData)
|
// - firewall Debug and Enabled are booleans parsed from source
|
||||||
|
logLevel, err := log.ParseLevel(allSettings.Log.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("log level: %w", err)
|
||||||
}
|
}
|
||||||
|
logger.Patch(log.SetLevel(logLevel))
|
||||||
|
netLinker.PatchLoggerLevel(logLevel)
|
||||||
|
|
||||||
allServers := storage.GetServers()
|
routingLogger := logger.New(log.SetComponent("routing"))
|
||||||
|
if *allSettings.Firewall.Debug { // To remove in v4
|
||||||
err = allSettings.Validate(allServers)
|
routingLogger.Patch(log.SetLevel(log.LevelDebug))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.PatchLevel(*allSettings.Log.Level)
|
|
||||||
|
|
||||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
|
||||||
|
|
||||||
const clientTimeout = 15 * time.Second
|
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
|
||||||
// Create configurators
|
|
||||||
alpineConf := alpine.New()
|
|
||||||
ovpnConf := openvpn.New(
|
|
||||||
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
|
|
||||||
cmder, puid, pgid)
|
|
||||||
dnsCrypto := dnscrypto.New(httpClient, "", "")
|
|
||||||
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
|
|
||||||
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
|
|
||||||
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
|
|
||||||
|
|
||||||
err = printVersions(ctx, logger, []printVersionElement{
|
|
||||||
{name: "Alpine", getVersion: alpineConf.Version},
|
|
||||||
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
|
|
||||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
|
||||||
{name: "Unbound", getVersion: dnsConf.Version},
|
|
||||||
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
|
|
||||||
return firewall.Version(ctx, cmder)
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(allSettings.String())
|
|
||||||
|
|
||||||
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultUsername = "nonrootuser"
|
|
||||||
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", errCreateUser, err)
|
|
||||||
}
|
|
||||||
if nonRootUsername != defaultUsername {
|
|
||||||
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
|
||||||
}
|
|
||||||
// set it for Unbound
|
|
||||||
// TODO remove this when migrating to qdm12/dns v2
|
|
||||||
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
|
|
||||||
allSettings.VPN.OpenVPN.ProcUser = nonRootUsername
|
|
||||||
|
|
||||||
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
firewallLogLevel := *allSettings.Log.Level
|
|
||||||
if *allSettings.Firewall.Debug {
|
|
||||||
firewallLogLevel = logging.LevelDebug
|
|
||||||
}
|
|
||||||
routingLogger := logger.NewChild(logging.Settings{
|
|
||||||
Prefix: "routing: ",
|
|
||||||
Level: firewallLogLevel,
|
|
||||||
})
|
|
||||||
routingConf := routing.New(netLinker, routingLogger)
|
routingConf := routing.New(netLinker, routingLogger)
|
||||||
|
|
||||||
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
|
defaultRoutes, err := routingConf.DefaultRoutes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -263,28 +218,102 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultIP, err := routingConf.DefaultIP()
|
firewallLogger := logger.New(log.SetComponent("firewall"))
|
||||||
|
if *allSettings.Firewall.Debug { // To remove in v4
|
||||||
|
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
|
||||||
|
}
|
||||||
|
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
|
||||||
|
defaultRoutes, localNetworks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
firewallLogger := logger.NewChild(logging.Settings{
|
if *allSettings.Firewall.Enabled {
|
||||||
Prefix: "firewall: ",
|
err = firewallConf.SetEnabled(ctx, true)
|
||||||
Level: firewallLogLevel,
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||||
|
storageLogger := logger.New(log.SetComponent("storage"))
|
||||||
|
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv6Supported, err := netLinker.IsIPv6Supported()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
|
||||||
|
pprofServer, err := pprof.New(allSettings.Pprof)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating Pprof server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||||
|
|
||||||
|
const clientTimeout = 15 * time.Second
|
||||||
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
|
// Create configurators
|
||||||
|
alpineConf := alpine.New()
|
||||||
|
ovpnConf := openvpn.New(
|
||||||
|
logger.New(log.SetComponent("openvpn configurator")),
|
||||||
|
cmder, puid, pgid)
|
||||||
|
|
||||||
|
err = printVersions(ctx, logger, []printVersionElement{
|
||||||
|
{name: "Alpine", getVersion: alpineConf.Version},
|
||||||
|
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||||
|
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||||
|
{name: "IPtables", getVersion: firewallConf.Version},
|
||||||
})
|
})
|
||||||
firewallConf := firewall.NewConfig(firewallLogger, cmder,
|
if err != nil {
|
||||||
defaultInterface, defaultGateway, localNetworks, defaultIP)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(allSettings.String())
|
||||||
|
|
||||||
|
for _, warning := range allSettings.Warnings() {
|
||||||
|
logger.Warn(warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = fs.FileMode(0o644)
|
||||||
|
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.MkdirAll("/gluetun", permission)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUsername = "nonrootuser"
|
||||||
|
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating user: %w", err)
|
||||||
|
}
|
||||||
|
if nonRootUsername != defaultUsername {
|
||||||
|
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
||||||
|
}
|
||||||
|
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
||||||
|
|
||||||
if err := routingConf.Setup(); err != nil {
|
if err := routingConf.Setup(); err != nil {
|
||||||
if strings.Contains(err.Error(), "operation not permitted") {
|
if strings.Contains(err.Error(), "operation not permitted") {
|
||||||
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: %s", errSetupRouting, err)
|
return fmt.Errorf("setting up routing: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Info("routing cleanup...")
|
routingLogger.Info("routing cleanup...")
|
||||||
if err := routingConf.TearDown(); err != nil {
|
if err := routingConf.TearDown(); err != nil {
|
||||||
logger.Error("cannot teardown routing: " + err.Error())
|
routingLogger.Error("cannot teardown routing: " + err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -295,25 +324,30 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tun.Check(constants.TunnelDevice); err != nil {
|
err = routingConf.AddLocalRules(localNetworks)
|
||||||
logger.Info(err.Error() + "; creating it...")
|
if err != nil {
|
||||||
err = tun.Create(constants.TunnelDevice)
|
return fmt.Errorf("adding local rules: %w", err)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *allSettings.Firewall.Enabled {
|
const tunDevice = "/dev/net/tun"
|
||||||
err := firewallConf.SetEnabled(ctx, true) // disabled by default
|
err = tun.Check(tunDevice)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
|
||||||
|
}
|
||||||
|
logger.Info(err.Error() + "; creating it...")
|
||||||
|
err = tun.Create(tunDevice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("creating tun device: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, port := range allSettings.Firewall.InputPorts {
|
for _, port := range allSettings.Firewall.InputPorts {
|
||||||
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
|
for _, defaultRoute := range defaultRoutes {
|
||||||
if err != nil {
|
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // TODO move inside firewall?
|
} // TODO move inside firewall?
|
||||||
|
|
||||||
@@ -328,58 +362,85 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
}
|
}
|
||||||
defaultGroupOptions := []group.Option{
|
defaultGroupOptions := []group.Option{
|
||||||
group.OptionTimeout(defaultShutdownTimeout),
|
group.OptionTimeout(defaultShutdownTimeout),
|
||||||
group.OptionOnSuccess(defaultShutdownOnSuccess)}
|
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
|
}
|
||||||
|
|
||||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||||
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
|
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
|
||||||
|
|
||||||
portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
|
if *allSettings.Pprof.Enabled {
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
// TODO run in run loop so this can be patched at runtime
|
||||||
httpClient, firewallConf, portForwardLogger)
|
pprofReady := make(chan struct{})
|
||||||
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
|
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
|
||||||
"port forwarding", goroutine.OptionTimeout(time.Second))
|
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
|
||||||
go portForwardLooper.Run(portForwardCtx, portForwardDone)
|
otherGroupHandler.Add(pprofHandler)
|
||||||
|
<-pprofReady
|
||||||
|
}
|
||||||
|
|
||||||
|
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||||
|
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||||
|
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
||||||
|
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||||
|
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||||
|
dnsLogger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating DNS loop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
|
|
||||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
|
||||||
unboundLogger)
|
|
||||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||||
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
||||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
go dnsLooper.Run(dnsCtx, dnsDone)
|
||||||
otherGroupHandler.Add(dnsHandler)
|
otherGroupHandler.Add(dnsHandler)
|
||||||
|
|
||||||
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||||
controlGroupHandler.Add(dnsTickerHandler)
|
controlGroupHandler.Add(dnsTickerHandler)
|
||||||
|
|
||||||
publicIPLooper := publicip.NewLoop(httpClient,
|
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
||||||
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
logger.New(log.SetComponent("ip getter")))
|
||||||
allSettings.PublicIP, puid, pgid)
|
if err != nil {
|
||||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
return fmt.Errorf("creating public ip loop: %w", err)
|
||||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
}
|
||||||
go publicIPLooper.Run(pubIPCtx, pubIPDone)
|
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||||
otherGroupHandler.Add(pubIPHandler)
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting public ip loop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
|
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
|
||||||
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
|
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
tickersGroupHandler.Add(pubIPTickerHandler)
|
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||||
|
healthChecker := healthcheck.NewChecker(healthLogger)
|
||||||
|
|
||||||
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
|
|
||||||
allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
unzipper := unzip.New(httpClient)
|
||||||
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
|
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||||
|
openvpnFileExtractor := extract.New()
|
||||||
|
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||||
|
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||||
|
openvpnFileExtractor, allSettings.Updater)
|
||||||
|
|
||||||
|
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||||
|
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||||
|
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
|
||||||
|
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||||
buildInfo, *allSettings.Version.Enabled)
|
buildInfo, *allSettings.Version.Enabled)
|
||||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||||
"vpn", goroutine.OptionTimeout(time.Second))
|
"vpn", goroutine.OptionTimeout(time.Second))
|
||||||
go vpnLooper.Run(vpnCtx, vpnDone)
|
go vpnLooper.Run(vpnCtx, vpnDone)
|
||||||
|
|
||||||
updaterLooper := updater.NewLooper(allSettings.Updater,
|
updaterLooper := updater.NewLoop(allSettings.Updater,
|
||||||
allServers, storage, vpnLooper.SetServers, httpClient,
|
providers, storage, httpClient, updaterLogger)
|
||||||
logger.NewChild(logging.Settings{Prefix: "updater: "}))
|
|
||||||
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
|
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
|
||||||
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
||||||
@@ -392,48 +453,64 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
controlGroupHandler.Add(updaterTickerHandler)
|
controlGroupHandler.Add(updaterTickerHandler)
|
||||||
|
|
||||||
httpProxyLooper := httpproxy.NewLoop(
|
httpProxyLooper := httpproxy.NewLoop(
|
||||||
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
|
logger.New(log.SetComponent("http proxy")),
|
||||||
allSettings.HTTPProxy)
|
allSettings.HTTPProxy)
|
||||||
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
|
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
|
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
|
||||||
otherGroupHandler.Add(httpProxyHandler)
|
otherGroupHandler.Add(httpProxyHandler)
|
||||||
|
|
||||||
shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks,
|
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
|
||||||
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
|
logger.New(log.SetComponent("shadowsocks")))
|
||||||
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
|
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
|
||||||
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||||
otherGroupHandler.Add(shadowsocksHandler)
|
otherGroupHandler.Add(shadowsocksHandler)
|
||||||
|
|
||||||
controlServerAddress := fmt.Sprintf(":%d", *allSettings.ControlServer.Port)
|
|
||||||
controlServerLogging := *allSettings.ControlServer.Log
|
|
||||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||||
logger.NewChild(logging.Settings{Prefix: "http server: "}),
|
logger.New(log.SetComponent("http server")),
|
||||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
|
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||||
go httpServer.Run(httpServerCtx, httpServerDone)
|
storage, ipv6Supported)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up control server: %w", err)
|
||||||
|
}
|
||||||
|
httpServerReady := make(chan struct{})
|
||||||
|
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
|
||||||
|
<-httpServerReady
|
||||||
controlGroupHandler.Add(httpServerHandler)
|
controlGroupHandler.Add(httpServerHandler)
|
||||||
|
|
||||||
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
|
|
||||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
|
||||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
|
||||||
|
|
||||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||||
order.OptionTimeout(totalShutdownTimeout),
|
order.OptionTimeout(totalShutdownTimeout),
|
||||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
order.OptionOnFailure(defaultShutdownOnFailure))
|
order.OptionOnFailure(defaultShutdownOnFailure))
|
||||||
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||||
vpnHandler, portForwardHandler, otherGroupHandler)
|
vpnHandler, otherGroupHandler)
|
||||||
|
|
||||||
// Start VPN for the first time in a blocking call
|
// Start VPN for the first time in a blocking call
|
||||||
// until the VPN is launched
|
// until the VPN is launched
|
||||||
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
||||||
|
|
||||||
<-ctx.Done()
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
stoppers := []interface {
|
||||||
|
String() string
|
||||||
|
Stop() error
|
||||||
|
}{
|
||||||
|
portForwardLooper, publicIPLooper,
|
||||||
|
}
|
||||||
|
for _, stopper := range stoppers {
|
||||||
|
err := stopper.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err := <-portForwardRunError:
|
||||||
|
logger.Errorf("port forwarding loop crashed: %s", err)
|
||||||
|
case err := <-publicIPRunError:
|
||||||
|
logger.Errorf("public IP loop crashed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return orderHandler.Shutdown(context.Background())
|
return orderHandler.Shutdown(context.Background())
|
||||||
}
|
}
|
||||||
@@ -448,7 +525,8 @@ type infoer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printVersions(ctx context.Context, logger infoer,
|
func printVersions(ctx context.Context, logger infoer,
|
||||||
elements []printVersionElement) (err error) {
|
elements []printVersionElement,
|
||||||
|
) (err error) {
|
||||||
const timeout = 5 * time.Second
|
const timeout = 5 * time.Second
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -456,10 +534,69 @@ func printVersions(ctx context.Context, logger infoer,
|
|||||||
for _, element := range elements {
|
for _, element := range elements {
|
||||||
version, err := element.getVersion(ctx)
|
version, err := element.getVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("getting %s version: %w", element.name, err)
|
||||||
}
|
}
|
||||||
logger.Info(element.name + " version: " + version)
|
logger.Info(element.name + " version: " + version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type netLinker interface {
|
||||||
|
Addresser
|
||||||
|
Router
|
||||||
|
Ruler
|
||||||
|
Linker
|
||||||
|
IsWireguardSupported() (ok bool, err error)
|
||||||
|
IsIPv6Supported() (ok bool, err error)
|
||||||
|
PatchLoggerLevel(level log.Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Addresser interface {
|
||||||
|
AddrList(link netlink.Link, family int) (
|
||||||
|
addresses []netlink.Addr, err error)
|
||||||
|
AddrReplace(link netlink.Link, addr netlink.Addr) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Router interface {
|
||||||
|
RouteList(family int) (routes []netlink.Route, err error)
|
||||||
|
RouteAdd(route netlink.Route) error
|
||||||
|
RouteDel(route netlink.Route) error
|
||||||
|
RouteReplace(route netlink.Route) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ruler interface {
|
||||||
|
RuleList(family int) (rules []netlink.Rule, err error)
|
||||||
|
RuleAdd(rule netlink.Rule) error
|
||||||
|
RuleDel(rule netlink.Rule) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Linker interface {
|
||||||
|
LinkList() (links []netlink.Link, err error)
|
||||||
|
LinkByName(name string) (link netlink.Link, err error)
|
||||||
|
LinkByIndex(index int) (link netlink.Link, err error)
|
||||||
|
LinkAdd(link netlink.Link) (linkIndex int, err error)
|
||||||
|
LinkDel(link netlink.Link) (err error)
|
||||||
|
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||||
|
LinkSetDown(link netlink.Link) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type clier interface {
|
||||||
|
ClientKey(args []string) error
|
||||||
|
FormatServers(args []string) error
|
||||||
|
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
|
||||||
|
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
|
||||||
|
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
|
||||||
|
GenKey(args []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tun interface {
|
||||||
|
Check(tunDevice string) error
|
||||||
|
Create(tunDevice string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunStarter interface {
|
||||||
|
Run(cmd *exec.Cmd) (output string, err error)
|
||||||
|
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, err error)
|
||||||
|
}
|
||||||
|
|||||||
89
go.mod
89
go.mod
@@ -1,46 +1,69 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.17
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.2.1
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/fatih/color v1.13.0
|
github.com/breml/rootcerts v0.3.3
|
||||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/qdm12/dns v1.11.0
|
github.com/klauspost/compress v1.18.1
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f
|
||||||
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.1.0
|
github.com/qdm12/gosplash v0.2.0
|
||||||
github.com/qdm12/gotree v0.2.0
|
github.com/qdm12/gotree v0.3.0
|
||||||
github.com/qdm12/govalid v0.1.0
|
github.com/qdm12/log v0.1.0
|
||||||
github.com/qdm12/ss-server v0.4.0
|
github.com/qdm12/ss-server v0.6.0
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
|
golang.org/x/net v0.47.0
|
||||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
|
golang.org/x/sys v0.38.0
|
||||||
|
golang.org/x/text v0.31.0
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.5.5 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.2.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/genetlink v1.0.0 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.4.0 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/miekg/dns v1.1.40 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.60.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
||||||
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
373
go.sum
373
go.sum
@@ -1,257 +1,192 @@
|
|||||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/breml/rootcerts v0.2.1 h1:GZMVDXOs945764NFck0vtHSjktKYubOFM0kjf5HAuwc=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/breml/rootcerts v0.2.1/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
||||||
|
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||||
|
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
|
||||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
|
||||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
|
||||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
|
||||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
|
||||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
|
||||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
|
||||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
|
||||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
|
||||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
|
||||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
|
||||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
|
||||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
|
||||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
|
||||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
|
||||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
|
||||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
|
||||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q=
|
|
||||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
|
||||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
|
||||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
|
||||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
|
||||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
|
||||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
|
||||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
|
||||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
|
||||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
|
||||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
|
||||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
|
||||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
|
||||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
|
||||||
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
|
|
||||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
|
||||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
|
||||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f h1:6wN5D9wACfmXDsQ366egVt0jXY4nqL/QnIwg4nWhXco=
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||||
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||||
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||||
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
|
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||||
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
|
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||||
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
|
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
||||||
github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
|
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||||
github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
|
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||||
github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
|
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
|
||||||
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
|
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
|
||||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
|
||||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||||
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
||||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
// Package alpine defines a configurator to interact with the Alpine operating system.
|
|
||||||
package alpine
|
package alpine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/user"
|
"os/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Alpiner = (*Alpine)(nil)
|
|
||||||
|
|
||||||
type Alpiner interface {
|
|
||||||
UserCreater
|
|
||||||
VersionGetter
|
|
||||||
}
|
|
||||||
|
|
||||||
type Alpine struct {
|
type Alpine struct {
|
||||||
alpineReleasePath string
|
alpineReleasePath string
|
||||||
passwdPath string
|
passwdPath string
|
||||||
|
|||||||
@@ -3,18 +3,13 @@ package alpine
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
ErrUserAlreadyExists = errors.New("user already exists")
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserCreater interface {
|
|
||||||
CreateUser(username string, uid int) (createdUsername string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateUser creates a user in Alpine with the given UID.
|
// CreateUser creates a user in Alpine with the given UID.
|
||||||
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||||
@@ -43,7 +38,8 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
|||||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VersionGetter interface {
|
func (a *Alpine) Version(context.Context) (version string, err error) {
|
||||||
Version(ctx context.Context) (version string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
|
|
||||||
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
|
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package cli
|
|||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
func (c *CLI) CI(context context.Context) error {
|
func (c *CLI) CI(context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
// Package cli defines an interface CLI to run command line operations.
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
var _ CLIer = (*CLI)(nil)
|
|
||||||
|
|
||||||
type CLIer interface {
|
|
||||||
ClientKeyFormatter
|
|
||||||
HealthChecker
|
|
||||||
OpenvpnConfigMaker
|
|
||||||
Updater
|
|
||||||
ServersFormatter
|
|
||||||
}
|
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
repoServersPath string
|
repoServersPath string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientKeyFormatter interface {
|
|
||||||
ClientKey(args []string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CLI) ClientKey(args []string) error {
|
func (c *CLI) ClientKey(args []string) error {
|
||||||
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
||||||
filepath := flagSet.String("path", constants.ClientKey, "file path to the client.key file")
|
const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate?
|
||||||
|
filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -32,9 +27,6 @@ func (c *CLI) ClientKey(args []string) error {
|
|||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s := string(data)
|
s := string(data)
|
||||||
s = strings.ReplaceAll(s, "\n", "")
|
s = strings.ReplaceAll(s, "\n", "")
|
||||||
s = strings.ReplaceAll(s, "\r", "")
|
s = strings.ReplaceAll(s, "\r", "")
|
||||||
|
|||||||
@@ -4,129 +4,109 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServersFormatter interface {
|
|
||||||
FormatServers(args []string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFormatNotRecognized = errors.New("format is not recognized")
|
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
||||||
ErrOpenOutputFile = errors.New("cannot open output file")
|
|
||||||
ErrWriteOutput = errors.New("cannot write to output file")
|
|
||||||
ErrCloseOutputFile = errors.New("cannot close output file")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
||||||
|
provider string, titleCaser cases.Caser,
|
||||||
|
) {
|
||||||
|
boolPtr, ok := providerToFormat[provider]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
||||||
|
}
|
||||||
|
flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers")
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CLI) FormatServers(args []string) error {
|
func (c *CLI) FormatServers(args []string) error {
|
||||||
var format, output string
|
var format, output string
|
||||||
var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad,
|
allProviders := providers.All()
|
||||||
nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark,
|
allProviderFlags := make([]string, len(allProviders))
|
||||||
torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool
|
for i, provider := range allProviders {
|
||||||
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
|
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
|
||||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
}
|
||||||
|
|
||||||
|
providersToFormat := make(map[string]*bool, len(allProviders))
|
||||||
|
for _, provider := range allProviderFlags {
|
||||||
|
providersToFormat[provider] = new(bool)
|
||||||
|
}
|
||||||
|
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
||||||
|
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
|
||||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||||
flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers")
|
titleCaser := cases.Title(language.English)
|
||||||
flagSet.BoolVar(&expressvpn, "expressvpn", false, "Format ExpressVPN servers")
|
for _, provider := range allProviderFlags {
|
||||||
flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers")
|
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
||||||
flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers")
|
}
|
||||||
flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers")
|
|
||||||
flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers")
|
|
||||||
flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers")
|
|
||||||
flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers")
|
|
||||||
flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers")
|
|
||||||
flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers")
|
|
||||||
flagSet.BoolVar(&privado, "privado", false, "Format Privado servers")
|
|
||||||
flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers")
|
|
||||||
flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers")
|
|
||||||
flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers")
|
|
||||||
flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers")
|
|
||||||
flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers")
|
|
||||||
flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers")
|
|
||||||
flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers")
|
|
||||||
flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers")
|
|
||||||
flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers")
|
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if format != "markdown" {
|
// Note the format is validated by storage.Format
|
||||||
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
|
|
||||||
|
// Verify only one provider is set to be formatted.
|
||||||
|
var providers []string
|
||||||
|
for provider, formatPtr := range providersToFormat {
|
||||||
|
if *formatPtr {
|
||||||
|
providers = append(providers, provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch len(providers) {
|
||||||
|
case 0:
|
||||||
|
return fmt.Errorf("%w", ErrProviderUnspecified)
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %d specified: %s",
|
||||||
|
ErrMultipleProvidersToFormat, len(providers),
|
||||||
|
strings.Join(providers, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerToFormat string
|
||||||
|
for _, providerToFormat = range allProviders {
|
||||||
|
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := newNoopLogger()
|
logger := newNoopLogger()
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrNewStorage, err)
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
}
|
}
|
||||||
currentServers := storage.GetServers()
|
|
||||||
|
|
||||||
var formatted string
|
formatted, err := storage.Format(providerToFormat, format)
|
||||||
switch {
|
if err != nil {
|
||||||
case cyberghost:
|
return fmt.Errorf("formatting servers: %w", err)
|
||||||
formatted = currentServers.Cyberghost.ToMarkdown()
|
|
||||||
case expressvpn:
|
|
||||||
formatted = currentServers.Expressvpn.ToMarkdown()
|
|
||||||
case fastestvpn:
|
|
||||||
formatted = currentServers.Fastestvpn.ToMarkdown()
|
|
||||||
case hideMyAss:
|
|
||||||
formatted = currentServers.HideMyAss.ToMarkdown()
|
|
||||||
case ipvanish:
|
|
||||||
formatted = currentServers.Ipvanish.ToMarkdown()
|
|
||||||
case ivpn:
|
|
||||||
formatted = currentServers.Ivpn.ToMarkdown()
|
|
||||||
case mullvad:
|
|
||||||
formatted = currentServers.Mullvad.ToMarkdown()
|
|
||||||
case nordvpn:
|
|
||||||
formatted = currentServers.Nordvpn.ToMarkdown()
|
|
||||||
case perfectPrivacy:
|
|
||||||
formatted = currentServers.Perfectprivacy.ToMarkdown()
|
|
||||||
case pia:
|
|
||||||
formatted = currentServers.Pia.ToMarkdown()
|
|
||||||
case privado:
|
|
||||||
formatted = currentServers.Privado.ToMarkdown()
|
|
||||||
case privatevpn:
|
|
||||||
formatted = currentServers.Privatevpn.ToMarkdown()
|
|
||||||
case protonvpn:
|
|
||||||
formatted = currentServers.Protonvpn.ToMarkdown()
|
|
||||||
case purevpn:
|
|
||||||
formatted = currentServers.Purevpn.ToMarkdown()
|
|
||||||
case surfshark:
|
|
||||||
formatted = currentServers.Surfshark.ToMarkdown()
|
|
||||||
case torguard:
|
|
||||||
formatted = currentServers.Torguard.ToMarkdown()
|
|
||||||
case vpnUnlimited:
|
|
||||||
formatted = currentServers.VPNUnlimited.ToMarkdown()
|
|
||||||
case vyprvpn:
|
|
||||||
formatted = currentServers.Vyprvpn.ToMarkdown()
|
|
||||||
case wevpn:
|
|
||||||
formatted = currentServers.Wevpn.ToMarkdown()
|
|
||||||
case windscribe:
|
|
||||||
formatted = currentServers.Windscribe.ToMarkdown()
|
|
||||||
default:
|
|
||||||
return ErrProviderUnspecified
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output = filepath.Clean(output)
|
output = filepath.Clean(output)
|
||||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrOpenOutputFile, err)
|
return fmt.Errorf("opening output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprint(file, formatted)
|
_, err = fmt.Fprint(file, formatted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return fmt.Errorf("%w: %s", ErrWriteOutput, err)
|
return fmt.Errorf("writing to output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrCloseOutputFile, err)
|
return fmt.Errorf("closing output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
66
internal/cli/genkey.go
Normal file
66
internal/cli/genkey.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CLI) GenKey(args []string) (err error) {
|
||||||
|
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
|
||||||
|
err = flagSet.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyLength = 128 / 8
|
||||||
|
keyBytes := make([]byte, keyLength)
|
||||||
|
|
||||||
|
_, _ = rand.Read(keyBytes)
|
||||||
|
|
||||||
|
key := base58Encode(keyBytes)
|
||||||
|
fmt.Println(key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58Encode(data []byte) string {
|
||||||
|
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
const radix = 58
|
||||||
|
|
||||||
|
zcount := 0
|
||||||
|
for zcount < len(data) && data[zcount] == 0 {
|
||||||
|
zcount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// integer simplification of ceil(log(256)/log(58))
|
||||||
|
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
|
||||||
|
size := zcount + ceilLog256Div58
|
||||||
|
|
||||||
|
output := make([]byte, size)
|
||||||
|
|
||||||
|
high := size - 1
|
||||||
|
for _, b := range data {
|
||||||
|
i := size - 1
|
||||||
|
for carry := uint32(b); i > high || carry != 0; i-- {
|
||||||
|
carry += 256 * uint32(output[i]) //nolint:mnd
|
||||||
|
output[i] = byte(carry % radix)
|
||||||
|
carry /= radix
|
||||||
|
}
|
||||||
|
high = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the additional "zero-gap" in the output buffer
|
||||||
|
additionalZeroGapEnd := zcount
|
||||||
|
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
|
||||||
|
additionalZeroGapEnd++
|
||||||
|
}
|
||||||
|
|
||||||
|
val := output[additionalZeroGapEnd-zcount:]
|
||||||
|
size = len(val)
|
||||||
|
for i := range val {
|
||||||
|
output[i] = alphabet[val[i]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output[:size])
|
||||||
|
}
|
||||||
@@ -6,21 +6,21 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthChecker interface {
|
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
|
||||||
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
|
|
||||||
// Extract the health server port from the configuration.
|
// Extract the health server port from the configuration.
|
||||||
config, err := source.ReadHealth()
|
var config settings.Health
|
||||||
|
err = config.Read(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.SetDefaults()
|
||||||
|
|
||||||
err = config.Validate()
|
err = config.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package sources
|
package cli
|
||||||
|
|
||||||
import "github.com/qdm12/gluetun/internal/configuration/settings"
|
import "github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
|
||||||
type Source interface {
|
type Source interface {
|
||||||
Read() (settings settings.Settings, err error)
|
Read() (settings settings.Settings, err error)
|
||||||
ReadHealth() (settings settings.Health, err error)
|
ReadHealth() (health settings.Health, err error)
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import "github.com/qdm12/golibs/logging"
|
|
||||||
|
|
||||||
type noopLogger struct{}
|
type noopLogger struct{}
|
||||||
|
|
||||||
func newNoopLogger() *noopLogger {
|
func newNoopLogger() *noopLogger {
|
||||||
return new(noopLogger)
|
return new(noopLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *noopLogger) Debug(s string) {}
|
func (l *noopLogger) Info(string) {}
|
||||||
func (l *noopLogger) Info(s string) {}
|
|
||||||
func (l *noopLogger) Warn(s string) {}
|
|
||||||
func (l *noopLogger) Error(s string) {}
|
|
||||||
func (l *noopLogger) PatchLevel(level logging.Level) {}
|
|
||||||
func (l *noopLogger) PatchPrefix(prefix string) {}
|
|
||||||
|
|||||||
@@ -1,50 +1,91 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenvpnConfigMaker interface {
|
|
||||||
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenvpnConfigLogger interface {
|
type OpenvpnConfigLogger interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
Warn(s string)
|
Warn(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
|
type Unzipper interface {
|
||||||
|
FetchAndExtract(ctx context.Context, url string) (
|
||||||
|
contents map[string][]byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParallelResolver interface {
|
||||||
|
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
|
||||||
|
hostToIPs map[string][]netip.Addr, warnings []string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPFetcher interface {
|
||||||
|
String() string
|
||||||
|
CanFetchAnyIP() bool
|
||||||
|
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPv6Checker interface {
|
||||||
|
IsIPv6Supported() (supported bool, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||||
|
ipv6Checker IPv6Checker,
|
||||||
|
) error {
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allServers := storage.GetServers()
|
|
||||||
|
|
||||||
allSettings, err := source.Read()
|
var allSettings settings.Settings
|
||||||
|
err = allSettings.Read(reader, logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
allSettings.SetDefaults()
|
||||||
|
|
||||||
|
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||||
|
return fmt.Errorf("validating settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused by this CLI command
|
||||||
|
unzipper := (Unzipper)(nil)
|
||||||
|
client := (*http.Client)(nil)
|
||||||
|
warner := (Warner)(nil)
|
||||||
|
parallelResolver := (ParallelResolver)(nil)
|
||||||
|
ipFetcher := (IPFetcher)(nil)
|
||||||
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
|
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||||
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||||
|
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||||
|
connection, err := providerConf.GetConnection(
|
||||||
|
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = allSettings.Validate(allServers); err != nil {
|
lines := providerConf.OpenVPNConfig(connection,
|
||||||
return err
|
allSettings.VPN.OpenVPN, ipv6Supported)
|
||||||
}
|
|
||||||
|
|
||||||
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
|
|
||||||
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(strings.Join(lines, "\n"))
|
fmt.Println(strings.Join(lines, "\n"))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,131 +2,122 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
|
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
"github.com/qdm12/gluetun/internal/updater"
|
"github.com/qdm12/gluetun/internal/updater"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||||
ErrDNSAddress = errors.New("DNS address is not valid")
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||||
ErrNewStorage = errors.New("cannot create storage")
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||||
ErrUpdateServerInformation = errors.New("cannot update server information")
|
|
||||||
ErrWriteToFile = errors.New("cannot write updated information to file")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Updater interface {
|
|
||||||
Update(ctx context.Context, args []string, logger UpdaterLogger) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdaterLogger interface {
|
type UpdaterLogger interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolPtr(b bool) *bool { return &b }
|
|
||||||
|
|
||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
options := settings.Updater{CLI: boolPtr(true)}
|
options := settings.Updater{}
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
var dnsAddress, csvProviders string
|
var csvProviders, ipToken, protonUsername, protonPassword string
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
||||||
flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||||
|
const defaultMinRatio = 0.8
|
||||||
|
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
||||||
|
"Minimum ratio of servers to find for the update to succeed")
|
||||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||||
|
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||||
|
flagSet.StringVar(&protonUsername, "proton-username", "", "Username to use to authenticate with Proton")
|
||||||
|
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !endUserMode && !maintainerMode {
|
if !endUserMode && !maintainerMode {
|
||||||
return ErrModeUnspecified
|
return fmt.Errorf("%w", ErrModeUnspecified)
|
||||||
}
|
|
||||||
|
|
||||||
options.DNSAddress = net.ParseIP(dnsAddress)
|
|
||||||
if options.DNSAddress == nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateAll {
|
if updateAll {
|
||||||
for _, provider := range constants.AllProviders() {
|
options.Providers = providers.All()
|
||||||
if provider == constants.Custom {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
options.Providers = append(options.Providers, provider)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if csvProviders == "" {
|
if csvProviders == "" {
|
||||||
return ErrNoProviderSpecified
|
return fmt.Errorf("%w", ErrNoProviderSpecified)
|
||||||
}
|
}
|
||||||
options.Providers = strings.Split(csvProviders, ",")
|
options.Providers = strings.Split(csvProviders, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
options.SetDefaults()
|
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||||
|
options.ProtonUsername = &protonUsername
|
||||||
|
options.ProtonPassword = &protonPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
options.SetDefaults(options.Providers[0])
|
||||||
|
|
||||||
err := options.Validate()
|
err := options.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("options validation failed: %w", err)
|
return fmt.Errorf("options validation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientTimeout = 10 * time.Second
|
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
|
||||||
|
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrNewStorage, err)
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
}
|
}
|
||||||
currentServers := storage.GetServers()
|
|
||||||
|
|
||||||
updater := updater.New(options, httpClient, currentServers, logger)
|
const clientTimeout = 10 * time.Second
|
||||||
allServers, err := updater.UpdateServers(ctx)
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
|
unzipper := unzip.New(httpClient)
|
||||||
|
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||||
|
nameTokenPairs := []api.NameToken{
|
||||||
|
{Name: string(api.IPInfo), Token: ipToken},
|
||||||
|
{Name: string(api.IP2Location)},
|
||||||
|
{Name: string(api.IfConfigCo)},
|
||||||
|
}
|
||||||
|
fetchers, err := api.New(nameTokenPairs, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
|
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||||
}
|
}
|
||||||
|
ipFetcher := api.NewResilient(fetchers, logger)
|
||||||
|
|
||||||
if endUserMode {
|
openvpnFileExtractor := extract.New()
|
||||||
if err := storage.FlushToFile(allServers); err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
|
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||||
}
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||||
|
|
||||||
|
updater := updater.New(httpClient, storage, providers, logger)
|
||||||
|
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updating server information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if maintainerMode {
|
if maintainerMode {
|
||||||
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
|
err := storage.FlushToFile(c.repoServersPath)
|
||||||
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeToEmbeddedJSON(repoServersPath string,
|
|
||||||
allServers models.AllServers) error {
|
|
||||||
const perms = 0600
|
|
||||||
f, err := os.OpenFile(repoServersPath,
|
|
||||||
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(f)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
return encoder.Encode(allServers)
|
|
||||||
}
|
|
||||||
|
|||||||
8
internal/command/cmder.go
Normal file
8
internal/command/cmder.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
// Cmder handles running subprograms synchronously and asynchronously.
|
||||||
|
type Cmder struct{}
|
||||||
|
|
||||||
|
func New() *Cmder {
|
||||||
|
return &Cmder{}
|
||||||
|
}
|
||||||
11
internal/command/interfaces_local.go
Normal file
11
internal/command/interfaces_local.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type execCmd interface {
|
||||||
|
CombinedOutput() ([]byte, error)
|
||||||
|
StdoutPipe() (io.ReadCloser, error)
|
||||||
|
StderrPipe() (io.ReadCloser, error)
|
||||||
|
Start() error
|
||||||
|
Wait() error
|
||||||
|
}
|
||||||
3
internal/command/mocks_generate_test.go
Normal file
3
internal/command/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
||||||
108
internal/command/mocks_local_test.go
Normal file
108
internal/command/mocks_local_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: interfaces_local.go
|
||||||
|
|
||||||
|
// Package command is a generated GoMock package.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
io "io"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockexecCmd is a mock of execCmd interface.
|
||||||
|
type MockexecCmd struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockexecCmdMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
|
||||||
|
type MockexecCmdMockRecorder struct {
|
||||||
|
mock *MockexecCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockexecCmd creates a new mock instance.
|
||||||
|
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
|
||||||
|
mock := &MockexecCmd{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockexecCmdMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombinedOutput mocks base method.
|
||||||
|
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CombinedOutput")
|
||||||
|
ret0, _ := ret[0].([]byte)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombinedOutput indicates an expected call of CombinedOutput.
|
||||||
|
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start mocks base method.
|
||||||
|
func (m *MockexecCmd) Start() error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Start")
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start indicates an expected call of Start.
|
||||||
|
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrPipe mocks base method.
|
||||||
|
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StderrPipe")
|
||||||
|
ret0, _ := ret[0].(io.ReadCloser)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrPipe indicates an expected call of StderrPipe.
|
||||||
|
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutPipe mocks base method.
|
||||||
|
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StdoutPipe")
|
||||||
|
ret0, _ := ret[0].(io.ReadCloser)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutPipe indicates an expected call of StdoutPipe.
|
||||||
|
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait mocks base method.
|
||||||
|
func (m *MockexecCmd) Wait() error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Wait")
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait indicates an expected call of Wait.
|
||||||
|
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
|
||||||
|
}
|
||||||
30
internal/command/run.go
Normal file
30
internal/command/run.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run runs a command in a blocking manner, returning its output and
|
||||||
|
// an error if it failed.
|
||||||
|
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
|
||||||
|
return run(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cmd execCmd) (output string, err error) {
|
||||||
|
stdout, err := cmd.CombinedOutput()
|
||||||
|
output = string(stdout)
|
||||||
|
output = strings.TrimSuffix(output, "\n")
|
||||||
|
lines := stringToLines(output)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = strings.TrimPrefix(lines[i], "'")
|
||||||
|
lines[i] = strings.TrimSuffix(lines[i], "'")
|
||||||
|
}
|
||||||
|
output = strings.Join(lines, "\n")
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToLines(s string) (lines []string) {
|
||||||
|
s = strings.TrimSuffix(s, "\n")
|
||||||
|
return strings.Split(s, "\n")
|
||||||
|
}
|
||||||
54
internal/command/run_test.go
Normal file
54
internal/command/run_test.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
errDummy := errors.New("dummy")
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
stdout []byte
|
||||||
|
cmdErr error
|
||||||
|
output string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no output": {},
|
||||||
|
"cmd error": {
|
||||||
|
stdout: []byte("'hello \nworld'\n"),
|
||||||
|
cmdErr: errDummy,
|
||||||
|
output: "hello \nworld",
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
mockCmd := NewMockexecCmd(ctrl)
|
||||||
|
|
||||||
|
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
|
||||||
|
|
||||||
|
output, err := run(mockCmd)
|
||||||
|
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.output, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
150
internal/command/split.go
Normal file
150
internal/command/split.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCommandEmpty = errors.New("command is empty")
|
||||||
|
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||||
|
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||||
|
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split splits a command string into a slice of arguments.
|
||||||
|
// This is especially important for commands such as:
|
||||||
|
// /bin/sh -c "echo hello"
|
||||||
|
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||||
|
// It supports backslash-escapes, single-quotes and double-quotes.
|
||||||
|
// It does not support:
|
||||||
|
// - the $" quoting style.
|
||||||
|
// - expansion (brace, shell or pathname).
|
||||||
|
func Split(command string) (words []string, err error) {
|
||||||
|
if command == "" {
|
||||||
|
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferSize = 1024
|
||||||
|
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||||
|
|
||||||
|
startIndex := 0
|
||||||
|
|
||||||
|
for startIndex < len(command) {
|
||||||
|
// skip any split characters at the start
|
||||||
|
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune(" \n\t", character):
|
||||||
|
startIndex += runeSize
|
||||||
|
case character == '\\':
|
||||||
|
// Look ahead to eventually skip an escaped newline
|
||||||
|
if command[startIndex+runeSize:] == "" {
|
||||||
|
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
||||||
|
}
|
||||||
|
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||||
|
if character == '\n' {
|
||||||
|
startIndex += runeSize + runeSize // backslash and newline
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
var word string
|
||||||
|
buffer.Reset()
|
||||||
|
word, startIndex, err = splitWord(command, startIndex, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
||||||
|
}
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: buffer must be cleared before calling this function.
|
||||||
|
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
cursor := startIndex
|
||||||
|
for cursor < len(input) {
|
||||||
|
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += runeLength
|
||||||
|
if character == '"' ||
|
||||||
|
character == '\'' ||
|
||||||
|
character == '\\' ||
|
||||||
|
character == ' ' ||
|
||||||
|
character == '\n' ||
|
||||||
|
character == '\t' {
|
||||||
|
buffer.WriteString(input[startIndex : cursor-runeLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune(" \n\t", character): // spacing character
|
||||||
|
return buffer.String(), cursor, nil
|
||||||
|
case character == '"':
|
||||||
|
return handleDoubleQuoted(input, cursor, buffer)
|
||||||
|
case character == '\'':
|
||||||
|
return handleSingleQuoted(input, cursor, buffer)
|
||||||
|
case character == '\\':
|
||||||
|
return handleEscaped(input, cursor, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(input[startIndex:])
|
||||||
|
return buffer.String(), len(input), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
cursor := startIndex
|
||||||
|
for cursor < len(input) {
|
||||||
|
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += nextRuneLength
|
||||||
|
switch nextCharacter {
|
||||||
|
case '"': // end of the double quoted string
|
||||||
|
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
||||||
|
return splitWord(input, cursor, buffer)
|
||||||
|
case '\\': // escaped character
|
||||||
|
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += escapedRuneLength
|
||||||
|
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
||||||
|
if escapedCharacter != '\n' {
|
||||||
|
// skip backslash entirely for the newline character
|
||||||
|
buffer.WriteRune(escapedCharacter)
|
||||||
|
}
|
||||||
|
startIndex = cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||||
|
if closingQuoteIndex == -1 {
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
||||||
|
}
|
||||||
|
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||||
|
const singleQuoteRuneLength = 1
|
||||||
|
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
||||||
|
return splitWord(input, startIndex, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
if input[startIndex:] == "" {
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
||||||
|
}
|
||||||
|
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||||
|
if character != '\n' { // backslash-escaped newline is ignored
|
||||||
|
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
||||||
|
}
|
||||||
|
startIndex += runeLength
|
||||||
|
return splitWord(input, startIndex, buffer)
|
||||||
|
}
|
||||||
110
internal/command/split_test.go
Normal file
110
internal/command/split_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Split(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
command string
|
||||||
|
words []string
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
command: "",
|
||||||
|
errWrapped: ErrCommandEmpty,
|
||||||
|
errMessage: "command is empty",
|
||||||
|
},
|
||||||
|
"concrete_sh_command": {
|
||||||
|
command: `/bin/sh -c "echo 123"`,
|
||||||
|
words: []string{"/bin/sh", "-c", "echo 123"},
|
||||||
|
},
|
||||||
|
"single_word": {
|
||||||
|
command: "word1",
|
||||||
|
words: []string{"word1"},
|
||||||
|
},
|
||||||
|
"two_words_single_space": {
|
||||||
|
command: "word1 word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"two_words_multiple_space": {
|
||||||
|
command: "word1 word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"two_words_no_expansion": {
|
||||||
|
command: "word1* word2?",
|
||||||
|
words: []string{"word1*", "word2?"},
|
||||||
|
},
|
||||||
|
"escaped_single quote": {
|
||||||
|
command: "ain\\'t good",
|
||||||
|
words: []string{"ain't", "good"},
|
||||||
|
},
|
||||||
|
"escaped_single_quote_all_single_quoted": {
|
||||||
|
command: "'ain'\\''t good'",
|
||||||
|
words: []string{"ain't good"},
|
||||||
|
},
|
||||||
|
"empty_single_quoted": {
|
||||||
|
command: "word1 '' word2",
|
||||||
|
words: []string{"word1", "", "word2"},
|
||||||
|
},
|
||||||
|
"escaped_newline": {
|
||||||
|
command: "word1\\\nword2",
|
||||||
|
words: []string{"word1word2"},
|
||||||
|
},
|
||||||
|
"quoted_newline": {
|
||||||
|
command: "text \"with\na\" quoted newline",
|
||||||
|
words: []string{"text", "with\na", "quoted", "newline"},
|
||||||
|
},
|
||||||
|
"quoted_escaped_newline": {
|
||||||
|
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
||||||
|
words: []string{"word1\\d\\\" word2word3 word4"},
|
||||||
|
},
|
||||||
|
"escaped_separated_newline": {
|
||||||
|
command: "word1 \\\n word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"double_quotes_no_spacing": {
|
||||||
|
command: "word1\"word2\"word3",
|
||||||
|
words: []string{"word1word2word3"},
|
||||||
|
},
|
||||||
|
"unterminated_single_quote": {
|
||||||
|
command: "'abc'\\''def",
|
||||||
|
errWrapped: ErrSingleQuoteUnterminated,
|
||||||
|
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||||
|
},
|
||||||
|
"unterminated_double_quote": {
|
||||||
|
command: "\"abc'def",
|
||||||
|
errWrapped: ErrDoubleQuoteUnterminated,
|
||||||
|
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||||
|
},
|
||||||
|
"unterminated_escape": {
|
||||||
|
command: "abc\\",
|
||||||
|
errWrapped: ErrEscapeUnterminated,
|
||||||
|
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||||
|
},
|
||||||
|
"unterminated_escape_only": {
|
||||||
|
command: " \\",
|
||||||
|
errWrapped: ErrEscapeUnterminated,
|
||||||
|
errMessage: `unterminated backslash-escape: " \\"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
words, err := Split(testCase.command)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.words, words)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
100
internal/command/start.go
Normal file
100
internal/command/start.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start launches a command and streams stdout and stderr to channels.
|
||||||
|
// All the channels returned are ready only and won't be closed
|
||||||
|
// if the command fails later.
|
||||||
|
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||||
|
stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, startErr error,
|
||||||
|
) {
|
||||||
|
return start(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, startErr error,
|
||||||
|
) {
|
||||||
|
stop := make(chan struct{})
|
||||||
|
stdoutReady := make(chan struct{})
|
||||||
|
stdoutLinesCh := make(chan string)
|
||||||
|
stdoutDone := make(chan struct{})
|
||||||
|
stderrReady := make(chan struct{})
|
||||||
|
stderrLinesCh := make(chan string)
|
||||||
|
stderrDone := make(chan struct{})
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
_ = stdout.Close()
|
||||||
|
close(stop)
|
||||||
|
<-stdoutDone
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
_ = stdout.Close()
|
||||||
|
_ = stderr.Close()
|
||||||
|
close(stop)
|
||||||
|
<-stdoutDone
|
||||||
|
<-stderrDone
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
waitErrorCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
err := cmd.Wait()
|
||||||
|
_ = stdout.Close()
|
||||||
|
_ = stderr.Close()
|
||||||
|
close(stop)
|
||||||
|
<-stdoutDone
|
||||||
|
<-stderrDone
|
||||||
|
waitErrorCh <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamToChannel(ready chan<- struct{},
|
||||||
|
stop <-chan struct{}, done chan<- struct{},
|
||||||
|
stream io.Reader, lines chan<- string,
|
||||||
|
) {
|
||||||
|
defer close(done)
|
||||||
|
close(ready)
|
||||||
|
scanner := bufio.NewScanner(stream)
|
||||||
|
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
|
||||||
|
const maxCapacity = 20 * 1024 * 1024 // 20MB
|
||||||
|
scanner.Buffer(lineBuffer, maxCapacity)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
// scanner is closed if the context is canceled
|
||||||
|
// or if the command failed starting because the
|
||||||
|
// stream is closed (io.EOF error).
|
||||||
|
lines <- scanner.Text()
|
||||||
|
}
|
||||||
|
err := scanner.Err()
|
||||||
|
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the error if it is stopped.
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
lines <- "stream error: " + err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/command/start_test.go
Normal file
118
internal/command/start_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func linesToReadCloser(lines []string) io.ReadCloser {
|
||||||
|
s := strings.Join(lines, "\n")
|
||||||
|
return io.NopCloser(bytes.NewBufferString(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_start(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
errDummy := errors.New("dummy")
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
stdout []string
|
||||||
|
stdoutPipeErr error
|
||||||
|
stderr []string
|
||||||
|
stderrPipeErr error
|
||||||
|
startErr error
|
||||||
|
waitErr error
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no output": {},
|
||||||
|
"success": {
|
||||||
|
stdout: []string{"hello", "world"},
|
||||||
|
stderr: []string{"some", "error"},
|
||||||
|
},
|
||||||
|
"stdout pipe error": {
|
||||||
|
stdoutPipeErr: errDummy,
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"stderr pipe error": {
|
||||||
|
stderrPipeErr: errDummy,
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"start error": {
|
||||||
|
startErr: errDummy,
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"wait error": {
|
||||||
|
waitErr: errDummy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
stdout := linesToReadCloser(testCase.stdout)
|
||||||
|
stderr := linesToReadCloser(testCase.stderr)
|
||||||
|
|
||||||
|
mockCmd := NewMockexecCmd(ctrl)
|
||||||
|
|
||||||
|
mockCmd.EXPECT().StdoutPipe().
|
||||||
|
Return(stdout, testCase.stdoutPipeErr)
|
||||||
|
if testCase.stdoutPipeErr == nil {
|
||||||
|
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
|
||||||
|
if testCase.stderrPipeErr == nil {
|
||||||
|
mockCmd.EXPECT().Start().Return(testCase.startErr)
|
||||||
|
if testCase.startErr == nil {
|
||||||
|
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutLines, stderrLines, waitError, err := start(mockCmd)
|
||||||
|
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
assert.Nil(t, stdoutLines)
|
||||||
|
assert.Nil(t, stderrLines)
|
||||||
|
assert.Nil(t, waitError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var stdoutIndex, stderrIndex int
|
||||||
|
|
||||||
|
done := false
|
||||||
|
for !done {
|
||||||
|
select {
|
||||||
|
case line := <-stdoutLines:
|
||||||
|
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
||||||
|
stdoutIndex++
|
||||||
|
case line := <-stderrLines:
|
||||||
|
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
||||||
|
stderrIndex++
|
||||||
|
case err := <-waitError:
|
||||||
|
if testCase.waitErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
||||||
|
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
internal/configuration/settings/deprecated.go
Normal file
27
internal/configuration/settings/deprecated.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||||
|
keyToMessage := map[string]string{
|
||||||
|
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||||
|
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||||
|
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||||
|
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
||||||
|
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||||
|
}
|
||||||
|
sortedKeys := maps.Keys(keyToMessage)
|
||||||
|
slices.Sort(sortedKeys)
|
||||||
|
warnings = make([]string, 0, len(keyToMessage))
|
||||||
|
for _, key := range sortedKeys {
|
||||||
|
if r.Get(key) != nil {
|
||||||
|
warnings = append(warnings, keyToMessage[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
@@ -1,37 +1,87 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNS contains settings to configure DNS.
|
// DNS contains settings to configure DNS.
|
||||||
type DNS struct {
|
type DNS struct {
|
||||||
|
// ServerEnabled is true if the server should be running
|
||||||
|
// and used. It defaults to true, and cannot be nil
|
||||||
|
// in the internal state.
|
||||||
|
ServerEnabled *bool
|
||||||
|
// UpstreamType can be dot or plain, and defaults to dot.
|
||||||
|
UpstreamType string `json:"upstream_type"`
|
||||||
|
// UpdatePeriod is the period to update DNS block lists.
|
||||||
|
// It can be set to 0 to disable the update.
|
||||||
|
// It defaults to 24h and cannot be nil in
|
||||||
|
// the internal state.
|
||||||
|
UpdatePeriod *time.Duration
|
||||||
|
// Providers is a list of DNS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// Blacklist contains settings to configure the filter
|
||||||
|
// block lists.
|
||||||
|
Blacklist DNSBlacklist
|
||||||
// ServerAddress is the DNS server to use inside
|
// ServerAddress is the DNS server to use inside
|
||||||
// the Go program and for the system.
|
// the Go program and for the system.
|
||||||
// It defaults to '127.0.0.1' to be used with the
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
// DoT server. It cannot be nil in the internal
|
// local server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress net.IP
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the Docker DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
// found in /etc/resolv.conf should be kept.
|
// found in /etc/resolv.conf should be used
|
||||||
// Note settings this to true will go around the
|
// Note setting this to true will likely DNS traffic
|
||||||
// DoT server blocking.
|
// outside the VPN tunnel since it would go through
|
||||||
|
// the local DNS server of your Docker/Kubernetes
|
||||||
|
// configuration, which is likely not going through the tunnel.
|
||||||
|
// This will also disable the DNS forwarder server and the
|
||||||
|
// `ServerAddress` field will be ignored.
|
||||||
// It defaults to false and cannot be nil in the
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
KeepNameserver *bool
|
||||||
// DOT contains settings to configure the DoT
|
|
||||||
// server.
|
|
||||||
DoT DoT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||||
|
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||||
|
)
|
||||||
|
|
||||||
func (d DNS) validate() (err error) {
|
func (d DNS) validate() (err error) {
|
||||||
err = d.DoT.validate()
|
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
||||||
|
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const minUpdatePeriod = 30 * time.Second
|
||||||
|
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||||
|
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||||
|
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed validating DoT settings: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -39,34 +89,64 @@ func (d DNS) validate() (err error) {
|
|||||||
|
|
||||||
func (d *DNS) Copy() (copied DNS) {
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
return DNS{
|
return DNS{
|
||||||
ServerAddress: helpers.CopyIP(d.ServerAddress),
|
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
||||||
KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
|
UpstreamType: d.UpstreamType,
|
||||||
DoT: d.DoT.copy(),
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
|
ServerAddress: d.ServerAddress,
|
||||||
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (d *DNS) mergeWith(other DNS) {
|
|
||||||
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
|
|
||||||
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
|
|
||||||
d.DoT.mergeWith(other.DoT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (d *DNS) overrideWith(other DNS) {
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
|
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
||||||
d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
|
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||||
d.DoT.overrideWith(other.DoT)
|
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||||
|
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||||
|
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||||
|
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||||
|
d.Blacklist.overrideWith(other.Blacklist)
|
||||||
|
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) setDefaults() {
|
func (d *DNS) setDefaults() {
|
||||||
localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
|
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
|
||||||
d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
|
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
|
||||||
d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
d.DoT.setDefaults()
|
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||||
|
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||||
|
provider.Cloudflare().Name,
|
||||||
|
})
|
||||||
|
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||||
|
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||||
|
d.Blacklist.setDefaults()
|
||||||
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
|
||||||
|
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
|
||||||
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
|
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
||||||
|
return d.ServerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
provider, err := providers.Get(d.Providers[0])
|
||||||
|
if err != nil {
|
||||||
|
// Settings should be validated before calling this function,
|
||||||
|
// so an error happening here is a programming error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.Plain.IPv4[0].Addr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DNS) String() string {
|
func (d DNS) String() string {
|
||||||
@@ -75,8 +155,77 @@ func (d DNS) String() string {
|
|||||||
|
|
||||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("DNS settings:")
|
node = gotree.New("DNS settings:")
|
||||||
|
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
|
||||||
|
if *d.KeepNameserver {
|
||||||
|
return node
|
||||||
|
}
|
||||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||||
node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver))
|
|
||||||
node.AppendNode(d.DoT.toLinesNode())
|
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
|
||||||
|
if !*d.ServerEnabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||||
|
|
||||||
|
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||||
|
for _, provider := range d.Providers {
|
||||||
|
upstreamResolvers.Append(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||||
|
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||||
|
|
||||||
|
update := "disabled"
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
|
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||||
|
|
||||||
|
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/blacklist"
|
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
"inet.af/netaddr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSBlacklist is settings for the DNS blacklist building.
|
// DNSBlacklist is settings for the DNS blacklist building.
|
||||||
@@ -18,21 +20,25 @@ type DNSBlacklist struct {
|
|||||||
BlockSurveillance *bool
|
BlockSurveillance *bool
|
||||||
AllowedHosts []string
|
AllowedHosts []string
|
||||||
AddBlockedHosts []string
|
AddBlockedHosts []string
|
||||||
AddBlockedIPs []netaddr.IP
|
AddBlockedIPs []netip.Addr
|
||||||
AddBlockedIPPrefixes []netaddr.IPPrefix
|
AddBlockedIPPrefixes []netip.Prefix
|
||||||
|
// RebindingProtectionExemptHostnames is a list of hostnames
|
||||||
|
// exempt from DNS rebinding protection.
|
||||||
|
RebindingProtectionExemptHostnames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DNSBlacklist) setDefaults() {
|
func (b *DNSBlacklist) setDefaults() {
|
||||||
b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true)
|
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
|
||||||
b.BlockAds = helpers.DefaultBool(b.BlockAds, false)
|
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
|
||||||
b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true)
|
b.BlockSurveillance = gosettings.DefaultPointer(b.BlockSurveillance, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||||
|
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b DNSBlacklist) validate() (err error) {
|
func (b DNSBlacklist) validate() (err error) {
|
||||||
@@ -48,51 +54,53 @@ func (b DNSBlacklist) validate() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||||
|
if !hostRegex.MatchString(host) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||||
return DNSBlacklist{
|
return DNSBlacklist{
|
||||||
BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious),
|
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||||
BlockAds: helpers.CopyBoolPtr(b.BlockAds),
|
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||||
BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance),
|
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||||
AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts),
|
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||||
AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts),
|
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||||
AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs),
|
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||||
AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes),
|
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||||
|
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
|
|
||||||
b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
|
|
||||||
b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
|
|
||||||
b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
|
|
||||||
b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
|
|
||||||
b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
|
|
||||||
b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
|
|
||||||
b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||||
b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
|
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
|
||||||
b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
|
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
|
||||||
b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
|
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
|
||||||
b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
|
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
|
||||||
b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||||
b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||||
b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||||
|
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
||||||
|
other.RebindingProtectionExemptHostnames)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
|
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||||
return blacklist.BuilderSettings{
|
settings blockbuilder.Settings,
|
||||||
BlockMalicious: *b.BlockMalicious,
|
) {
|
||||||
BlockAds: *b.BlockAds,
|
return blockbuilder.Settings{
|
||||||
BlockSurveillance: *b.BlockSurveillance,
|
Client: client,
|
||||||
|
BlockMalicious: b.BlockMalicious,
|
||||||
|
BlockAds: b.BlockAds,
|
||||||
|
BlockSurveillance: b.BlockSurveillance,
|
||||||
AllowedHosts: b.AllowedHosts,
|
AllowedHosts: b.AllowedHosts,
|
||||||
AddBlockedHosts: b.AddBlockedHosts,
|
AddBlockedHosts: b.AddBlockedHosts,
|
||||||
AddBlockedIPs: b.AddBlockedIPs,
|
AddBlockedIPs: b.AddBlockedIPs,
|
||||||
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) String() string {
|
func (b DNSBlacklist) String() string {
|
||||||
@@ -102,37 +110,130 @@ func (b DNSBlacklist) String() string {
|
|||||||
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("DNS filtering settings:")
|
node = gotree.New("DNS filtering settings:")
|
||||||
|
|
||||||
node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious))
|
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
|
||||||
node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds))
|
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
|
||||||
node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance))
|
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
||||||
|
|
||||||
if len(b.AllowedHosts) > 0 {
|
if len(b.AllowedHosts) > 0 {
|
||||||
allowedHostsNode := node.Appendf("Allowed hosts:")
|
allowedHostsNode := node.Append("Allowed hosts:")
|
||||||
for _, host := range b.AllowedHosts {
|
for _, host := range b.AllowedHosts {
|
||||||
allowedHostsNode.Appendf(host)
|
allowedHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedHosts) > 0 {
|
if len(b.AddBlockedHosts) > 0 {
|
||||||
blockedHostsNode := node.Appendf("Blocked hosts:")
|
blockedHostsNode := node.Append("Blocked hosts:")
|
||||||
for _, host := range b.AddBlockedHosts {
|
for _, host := range b.AddBlockedHosts {
|
||||||
blockedHostsNode.Appendf(host)
|
blockedHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedIPs) > 0 {
|
if len(b.AddBlockedIPs) > 0 {
|
||||||
blockedIPsNode := node.Appendf("Blocked IP addresses:")
|
blockedIPsNode := node.Append("Blocked IP addresses:")
|
||||||
for _, ip := range b.AddBlockedIPs {
|
for _, ip := range b.AddBlockedIPs {
|
||||||
blockedIPsNode.Appendf(ip.String())
|
blockedIPsNode.Append(ip.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedIPPrefixes) > 0 {
|
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||||
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
|
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
|
||||||
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||||
blockedIPPrefixesNode.Appendf(ipNetwork.String())
|
blockedIPPrefixesNode.Append(ipNetwork.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
||||||
|
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
||||||
|
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||||
|
exemptHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||||
|
b.BlockMalicious, err = r.BoolPtr("BLOCK_MALICIOUS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
|
||||||
|
reader.RetroKeys("BLOCK_NSA"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||||
|
|
||||||
|
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||||
|
ipPrefixes []netip.Prefix, err error,
|
||||||
|
) {
|
||||||
|
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO v4 remove this block below
|
||||||
|
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ips = append(ips, privateIPs...)
|
||||||
|
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||||
|
|
||||||
|
return ips, ipPrefixes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
|
||||||
|
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||||
|
ipPrefixes []netip.Prefix, err error,
|
||||||
|
) {
|
||||||
|
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||||
|
if len(privateAddresses) == 0 {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ips = make([]netip.Addr, 0, len(privateAddresses))
|
||||||
|
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses))
|
||||||
|
|
||||||
|
for _, privateAddress := range privateAddresses {
|
||||||
|
ip, err := netip.ParseAddr(privateAddress)
|
||||||
|
if err == nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipPrefix, err := netip.ParsePrefix(privateAddress)
|
||||||
|
if err == nil {
|
||||||
|
ipPrefixes = append(ipPrefixes, ipPrefix)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf(
|
||||||
|
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
|
||||||
|
ErrPrivateAddressNotValid, privateAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, ipPrefixes, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DoT contains settings to configure the DoT server.
|
|
||||||
type DoT struct {
|
|
||||||
// Enabled is true if the DoT server should be running
|
|
||||||
// and used. It defaults to true, and cannot be nil
|
|
||||||
// in the internal state.
|
|
||||||
Enabled *bool
|
|
||||||
// UpdatePeriod is the period to update DNS block
|
|
||||||
// lists and cryptographic files for DNSSEC validation.
|
|
||||||
// It can be set to 0 to disable the update.
|
|
||||||
// It defaults to 24h and cannot be nil in
|
|
||||||
// the internal state.
|
|
||||||
UpdatePeriod *time.Duration
|
|
||||||
// Unbound contains settings to configure Unbound.
|
|
||||||
Unbound Unbound
|
|
||||||
// Blacklist contains settings to configure the filter
|
|
||||||
// block lists.
|
|
||||||
Blacklist DNSBlacklist
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d DoT) validate() (err error) {
|
|
||||||
const minUpdatePeriod = 30 * time.Second
|
|
||||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
|
||||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Unbound.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) copy() (copied DoT) {
|
|
||||||
return DoT{
|
|
||||||
Enabled: helpers.CopyBoolPtr(d.Enabled),
|
|
||||||
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
|
|
||||||
Unbound: d.Unbound.copy(),
|
|
||||||
Blacklist: d.Blacklist.copy(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (d *DoT) mergeWith(other DoT) {
|
|
||||||
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
|
|
||||||
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
|
||||||
d.Unbound.mergeWith(other.Unbound)
|
|
||||||
d.Blacklist.mergeWith(other.Blacklist)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
|
||||||
// settings object with any field set in the other
|
|
||||||
// settings.
|
|
||||||
func (d *DoT) overrideWith(other DoT) {
|
|
||||||
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
|
|
||||||
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
|
||||||
d.Unbound.overrideWith(other.Unbound)
|
|
||||||
d.Blacklist.overrideWith(other.Blacklist)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) setDefaults() {
|
|
||||||
d.Enabled = helpers.DefaultBool(d.Enabled, true)
|
|
||||||
const defaultUpdatePeriod = 24 * time.Hour
|
|
||||||
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
|
|
||||||
d.Unbound.setDefaults()
|
|
||||||
d.Blacklist.setDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) String() string {
|
|
||||||
return d.toLinesNode().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
|
||||||
node = gotree.New("DNS over TLS settings:")
|
|
||||||
|
|
||||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled))
|
|
||||||
if !*d.Enabled {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
update := "disabled"
|
|
||||||
if *d.UpdatePeriod > 0 {
|
|
||||||
update = "every " + d.UpdatePeriod.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Update period: %s", update)
|
|
||||||
|
|
||||||
node.AppendNode(d.Unbound.toLinesNode())
|
|
||||||
node.AppendNode(d.Blacklist.toLinesNode())
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
@@ -3,21 +3,24 @@ package settings
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrValueUnknown = errors.New("value is unknown")
|
||||||
ErrCityNotValid = errors.New("the city specified is not valid")
|
ErrCityNotValid = errors.New("the city specified is not valid")
|
||||||
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
|
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
|
||||||
|
ErrCategoryNotValid = errors.New("the category specified is not valid")
|
||||||
ErrCountryNotValid = errors.New("the country specified is not valid")
|
ErrCountryNotValid = errors.New("the country specified is not valid")
|
||||||
ErrFirewallZeroPort = errors.New("cannot have a zero port to block")
|
ErrFilepathMissing = errors.New("filepath is missing")
|
||||||
|
ErrFirewallZeroPort = errors.New("cannot have a zero port")
|
||||||
|
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
|
||||||
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
|
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
|
||||||
ErrISPNotValid = errors.New("the ISP specified is not valid")
|
ErrISPNotValid = errors.New("the ISP specified is not valid")
|
||||||
|
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
|
||||||
|
ErrMissingValue = errors.New("missing value")
|
||||||
ErrNameNotValid = errors.New("the server name specified is not valid")
|
ErrNameNotValid = errors.New("the server name specified is not valid")
|
||||||
ErrOpenVPNClientCertMissing = errors.New("client certificate is missing")
|
|
||||||
ErrOpenVPNClientCertNotValid = errors.New("client certificate is not valid")
|
|
||||||
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
|
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
|
||||||
ErrOpenVPNClientKeyNotValid = errors.New("client key is not valid")
|
|
||||||
ErrOpenVPNConfigFile = errors.New("custom configuration file error")
|
|
||||||
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
||||||
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
||||||
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
||||||
|
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
|
||||||
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
||||||
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
||||||
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
||||||
@@ -25,27 +28,31 @@ var (
|
|||||||
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
|
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
|
||||||
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
|
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
|
||||||
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||||
ErrPortForwardingFilepathNotValid = errors.New("port forwarding filepath given is not valid")
|
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
|
||||||
ErrPublicIPFilepathNotValid = errors.New("public IP address file path is not valid")
|
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
|
||||||
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
|
|
||||||
ErrRegionNotValid = errors.New("the region specified is not valid")
|
ErrRegionNotValid = errors.New("the region specified is not valid")
|
||||||
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||||
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||||
|
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||||
|
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||||
|
ErrUpdaterProtonUsernameMissing = errors.New("proton username is missing")
|
||||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||||
|
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||||
|
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
|
||||||
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
||||||
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
||||||
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
||||||
|
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
|
||||||
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
|
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
|
||||||
|
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
|
||||||
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
|
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
|
||||||
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
|
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
|
||||||
ErrWireguardPreSharedKeyNotValid = errors.New("pre-shared key is not valid")
|
|
||||||
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
|
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
|
||||||
ErrWireguardPrivateKeyNotValid = errors.New("private key is not valid")
|
|
||||||
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
|
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
|
||||||
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
|
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
|
||||||
|
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
|
||||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ import (
|
|||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
VPNInputPorts []uint16
|
VPNInputPorts []uint16
|
||||||
InputPorts []uint16
|
InputPorts []uint16
|
||||||
OutboundSubnets []net.IPNet
|
OutboundSubnets []netip.Prefix
|
||||||
Enabled *bool
|
Enabled *bool
|
||||||
Debug *bool
|
Debug *bool
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,12 @@ func (f Firewall) validate() (err error) {
|
|||||||
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
|
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, subnet := range f.OutboundSubnets {
|
||||||
|
if subnet.Addr().IsUnspecified() {
|
||||||
|
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,40 +47,28 @@ func hasZeroPort(ports []uint16) (has bool) {
|
|||||||
|
|
||||||
func (f *Firewall) copy() (copied Firewall) {
|
func (f *Firewall) copy() (copied Firewall) {
|
||||||
return Firewall{
|
return Firewall{
|
||||||
VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts),
|
VPNInputPorts: gosettings.CopySlice(f.VPNInputPorts),
|
||||||
InputPorts: helpers.CopyUint16Slice(f.InputPorts),
|
InputPorts: gosettings.CopySlice(f.InputPorts),
|
||||||
OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets),
|
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
|
||||||
Enabled: helpers.CopyBoolPtr(f.Enabled),
|
Enabled: gosettings.CopyPointer(f.Enabled),
|
||||||
Debug: helpers.CopyBoolPtr(f.Debug),
|
Debug: gosettings.CopyPointer(f.Debug),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
// It merges values of slices together, even if they
|
|
||||||
// are set in the receiver settings.
|
|
||||||
func (f *Firewall) mergeWith(other Firewall) {
|
|
||||||
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
|
|
||||||
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
|
|
||||||
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
|
|
||||||
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
|
|
||||||
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (f *Firewall) overrideWith(other Firewall) {
|
func (f *Firewall) overrideWith(other Firewall) {
|
||||||
f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts)
|
f.VPNInputPorts = gosettings.OverrideWithSlice(f.VPNInputPorts, other.VPNInputPorts)
|
||||||
f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts)
|
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
|
||||||
f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets)
|
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
|
||||||
f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled)
|
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
|
||||||
f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug)
|
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firewall) setDefaults() {
|
func (f *Firewall) setDefaults() {
|
||||||
f.Enabled = helpers.DefaultBool(f.Enabled, true)
|
f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
|
||||||
f.Debug = helpers.DefaultBool(f.Debug, false)
|
f.Debug = gosettings.DefaultPointer(f.Debug, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Firewall) String() string {
|
func (f Firewall) String() string {
|
||||||
@@ -83,7 +78,7 @@ func (f Firewall) String() string {
|
|||||||
func (f Firewall) toLinesNode() (node *gotree.Node) {
|
func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Firewall settings:")
|
node = gotree.New("Firewall settings:")
|
||||||
|
|
||||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled))
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(f.Enabled))
|
||||||
if !*f.Enabled {
|
if !*f.Enabled {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -109,9 +104,39 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
|
|||||||
if len(f.OutboundSubnets) > 0 {
|
if len(f.OutboundSubnets) > 0 {
|
||||||
outboundSubnets := node.Appendf("Outbound subnets:")
|
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||||
for _, subnet := range f.OutboundSubnets {
|
for _, subnet := range f.OutboundSubnets {
|
||||||
outboundSubnets.Appendf("%s", subnet)
|
outboundSubnets.Appendf("%s", &subnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Firewall) read(r *reader.Reader) (err error) {
|
||||||
|
f.VPNInputPorts, err = r.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.InputPorts, err = r.CSVUint16("FIREWALL_INPUT_PORTS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.OutboundSubnets, err = r.CSVNetipPrefixes(
|
||||||
|
"FIREWALL_OUTBOUND_SUBNETS", reader.RetroKeys("EXTRA_SUBNETS"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Enabled, err = r.BoolPtr("FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
73
internal/configuration/settings/firewall_test.go
Normal file
73
internal/configuration/settings/firewall_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Firewall_validate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
firewall Firewall
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"zero_vpn_input_port": {
|
||||||
|
firewall: Firewall{
|
||||||
|
VPNInputPorts: []uint16{0},
|
||||||
|
},
|
||||||
|
errWrapped: ErrFirewallZeroPort,
|
||||||
|
errMessage: "VPN input ports: cannot have a zero port",
|
||||||
|
},
|
||||||
|
"zero_input_port": {
|
||||||
|
firewall: Firewall{
|
||||||
|
InputPorts: []uint16{0},
|
||||||
|
},
|
||||||
|
errWrapped: ErrFirewallZeroPort,
|
||||||
|
errMessage: "input ports: cannot have a zero port",
|
||||||
|
},
|
||||||
|
"unspecified_outbound_subnet": {
|
||||||
|
firewall: Firewall{
|
||||||
|
OutboundSubnets: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errWrapped: ErrFirewallPublicOutboundSubnet,
|
||||||
|
errMessage: "outbound subnet has an unspecified address: 0.0.0.0/0",
|
||||||
|
},
|
||||||
|
"public_outbound_subnet": {
|
||||||
|
firewall: Firewall{
|
||||||
|
OutboundSubnets: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"valid_settings": {
|
||||||
|
firewall: Firewall{
|
||||||
|
VPNInputPorts: []uint16{100, 101},
|
||||||
|
InputPorts: []uint16{200, 201},
|
||||||
|
OutboundSubnets: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
netip.MustParsePrefix("10.10.1.1/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := testCase.firewall.validate()
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
"github.com/qdm12/govalid/address"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Health contains settings for the healthcheck and health server.
|
// Health contains settings for the healthcheck and health server.
|
||||||
@@ -15,25 +17,23 @@ type Health struct {
|
|||||||
// for the health check server.
|
// for the health check server.
|
||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
// AddressToPing is the IP address or domain name to
|
// TargetAddress is the address (host or host:port)
|
||||||
// ping periodically for the health check.
|
// to TCP TLS dial to periodically for the health check.
|
||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
AddressToPing string
|
TargetAddress string
|
||||||
VPN HealthyWait
|
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
||||||
|
// in the health checker. It can be set to an unspecified address (0.0.0.0)
|
||||||
|
// such that the VPN server IP is used, which is also the default behavior.
|
||||||
|
ICMPTargetIP netip.Addr
|
||||||
|
// RestartVPN indicates whether to restart the VPN connection
|
||||||
|
// when the healthcheck fails.
|
||||||
|
RestartVPN *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) Validate() (err error) {
|
func (h Health) Validate() (err error) {
|
||||||
uid := os.Getuid()
|
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
||||||
_, err = address.Validate(h.ServerAddress,
|
|
||||||
address.OptionListening(uid))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s",
|
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||||
ErrServerAddressNotValid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.VPN.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("health VPN settings validation failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -42,32 +42,27 @@ func (h Health) Validate() (err error) {
|
|||||||
func (h *Health) copy() (copied Health) {
|
func (h *Health) copy() (copied Health) {
|
||||||
return Health{
|
return Health{
|
||||||
ServerAddress: h.ServerAddress,
|
ServerAddress: h.ServerAddress,
|
||||||
AddressToPing: h.AddressToPing,
|
TargetAddress: h.TargetAddress,
|
||||||
VPN: h.VPN.copy(),
|
ICMPTargetIP: h.ICMPTargetIP,
|
||||||
|
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (h *Health) MergeWith(other Health) {
|
|
||||||
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
|
|
||||||
h.AddressToPing = helpers.MergeWithString(h.AddressToPing, other.AddressToPing)
|
|
||||||
h.VPN.mergeWith(other.VPN)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideWith overrides fields of the receiver
|
// OverrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (h *Health) OverrideWith(other Health) {
|
func (h *Health) OverrideWith(other Health) {
|
||||||
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
|
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||||
h.AddressToPing = helpers.OverrideWithString(h.AddressToPing, other.AddressToPing)
|
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||||
h.VPN.overrideWith(other.VPN)
|
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
||||||
|
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) SetDefaults() {
|
func (h *Health) SetDefaults() {
|
||||||
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
|
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||||
h.AddressToPing = helpers.DefaultString(h.AddressToPing, "github.com")
|
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||||
h.VPN.setDefaults()
|
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
||||||
|
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) String() string {
|
func (h Health) String() string {
|
||||||
@@ -77,7 +72,27 @@ func (h Health) String() string {
|
|||||||
func (h Health) toLinesNode() (node *gotree.Node) {
|
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Health settings:")
|
node = gotree.New("Health settings:")
|
||||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||||
node.Appendf("Address to ping: %s", h.AddressToPing)
|
node.Appendf("Target address: %s", h.TargetAddress)
|
||||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
icmpTarget := "VPN server IP"
|
||||||
|
if !h.ICMPTargetIP.IsUnspecified() {
|
||||||
|
icmpTarget = h.ICMPTargetIP.String()
|
||||||
|
}
|
||||||
|
node.Appendf("ICMP target IP: %s", icmpTarget)
|
||||||
|
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Health) Read(r *reader.Reader) (err error) {
|
||||||
|
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||||
|
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
||||||
|
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
||||||
|
h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthyWait struct {
|
|
||||||
// Initial is the initial duration to wait for the program
|
|
||||||
// to be healthy before taking action.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
Initial *time.Duration
|
|
||||||
// Addition is the duration to add to the Initial duration
|
|
||||||
// after Initial has expired to wait longer for the program
|
|
||||||
// to be healthy.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
Addition *time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) validate() (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (h *HealthyWait) copy() (copied HealthyWait) {
|
|
||||||
return HealthyWait{
|
|
||||||
Initial: helpers.CopyDurationPtr(h.Initial),
|
|
||||||
Addition: helpers.CopyDurationPtr(h.Addition),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (h *HealthyWait) mergeWith(other HealthyWait) {
|
|
||||||
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
|
|
||||||
h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
|
||||||
// settings object with any field set in the other
|
|
||||||
// settings.
|
|
||||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
|
||||||
h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
|
|
||||||
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthyWait) setDefaults() {
|
|
||||||
const initialDurationDefault = 6 * time.Second
|
|
||||||
const additionDurationDefault = 5 * time.Second
|
|
||||||
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
|
|
||||||
h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) String() string {
|
|
||||||
return h.toLinesNode("Health").String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
|
||||||
node = gotree.New(kind + " wait durations:")
|
|
||||||
node.Appendf("Initial duration: %s", *h.Initial)
|
|
||||||
node.Appendf("Additional duration: %s", *h.Addition)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
5
internal/configuration/settings/helpers.go
Normal file
5
internal/configuration/settings/helpers.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
func ptrTo[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsOneOf(value string, choices ...string) (ok bool) {
|
|
||||||
for _, choice := range choices {
|
for _, choice := range choices {
|
||||||
if value == choice {
|
if value == choice {
|
||||||
return true
|
return true
|
||||||
@@ -14,32 +8,3 @@ func IsOneOf(value string, choices ...string) (ok bool) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrValueNotOneOf = errors.New("value is not one of the possible choices")
|
|
||||||
|
|
||||||
func AreAllOneOf(values, choices []string) (err error) {
|
|
||||||
set := make(map[string]struct{}, len(choices))
|
|
||||||
for _, choice := range choices {
|
|
||||||
choice = strings.ToLower(choice)
|
|
||||||
set[choice] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range values {
|
|
||||||
_, ok := set[value]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%w: value %q, choices available are %s",
|
|
||||||
ErrValueNotOneOf, value, strings.Join(choices, ", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) {
|
|
||||||
for _, choice := range choices {
|
|
||||||
if port == choice {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
"inet.af/netaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CopyStringPtr(original *string) (copied *string) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(string)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyBoolPtr(original *bool) (copied *bool) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(bool)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyUint8Ptr(original *uint8) (copied *uint8) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(uint8)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyUint16Ptr(original *uint16) (copied *uint16) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(uint16)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIntPtr(original *int) (copied *int) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(int)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyDurationPtr(original *time.Duration) (copied *time.Duration) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(time.Duration)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = new(logging.Level)
|
|
||||||
*copied = *original
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIP(original net.IP) (copied net.IP) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
copied = make(net.IP, len(original))
|
|
||||||
copy(copied, original)
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIPNet(original net.IPNet) (copied net.IPNet) {
|
|
||||||
if original.IP != nil {
|
|
||||||
copied.IP = make(net.IP, len(original.IP))
|
|
||||||
copy(copied.IP, original.IP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if original.Mask != nil {
|
|
||||||
copied.Mask = make(net.IPMask, len(original.Mask))
|
|
||||||
copy(copied.Mask, original.Mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = new(net.IPNet)
|
|
||||||
*copied = CopyIPNet(*original)
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) {
|
|
||||||
b, err := original.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = copied.UnmarshalBinary(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) {
|
|
||||||
b, err := original.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = copied.UnmarshalText(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyStringSlice(original []string) (copied []string) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = make([]string, len(original))
|
|
||||||
copy(copied, original)
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyUint16Slice(original []uint16) (copied []uint16) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = make([]uint16, len(original))
|
|
||||||
copy(copied, original)
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = make([]net.IPNet, len(original))
|
|
||||||
for i := range original {
|
|
||||||
copied[i] = CopyIPNet(original[i])
|
|
||||||
}
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = make([]netaddr.IPPrefix, len(original))
|
|
||||||
for i := range original {
|
|
||||||
copied[i] = CopyIPPrefix(original[i])
|
|
||||||
}
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) {
|
|
||||||
if original == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
copied = make([]netaddr.IP, len(original))
|
|
||||||
for i := range original {
|
|
||||||
copied[i] = CopyNetaddrIP(original[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return copied
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DefaultInt(existing *int, defaultValue int) (
|
|
||||||
result *int) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(int)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultUint8(existing *uint8, defaultValue uint8) (
|
|
||||||
result *uint8) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(uint8)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultUint16(existing *uint16, defaultValue uint16) (
|
|
||||||
result *uint16) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(uint16)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultBool(existing *bool, defaultValue bool) (
|
|
||||||
result *bool) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(bool)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultString(existing string, defaultValue string) (
|
|
||||||
result string) {
|
|
||||||
if existing != "" {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultStringPtr(existing *string, defaultValue string) (result *string) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(string)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultDuration(existing *time.Duration,
|
|
||||||
defaultValue time.Duration) (result *time.Duration) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(time.Duration)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultLogLevel(existing *logging.Level,
|
|
||||||
defaultValue logging.Level) (result *logging.Level) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(logging.Level)
|
|
||||||
*result = defaultValue
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultIP(existing net.IP, defaultValue net.IP) (
|
|
||||||
result net.IP) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFileDoesNotExist = errors.New("file does not exist")
|
|
||||||
ErrFileRead = errors.New("cannot read file")
|
|
||||||
ErrFileClose = errors.New("cannot close file")
|
|
||||||
)
|
|
||||||
|
|
||||||
func FileExists(path string) (err error) {
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return fmt.Errorf("%w: %s", ErrFileDoesNotExist, path)
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrFileRead, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrFileClose, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
"inet.af/netaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MergeWithBool(existing, other *bool) (result *bool) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(bool)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithString(existing, other string) (result string) {
|
|
||||||
if existing != "" {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
return other
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithStringPtr(existing, other *string) (result *string) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(string)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithInt(existing, other *int) (result *int) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(int)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithUint8(existing, other *uint8) (result *uint8) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(uint8)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithUint16(existing, other *uint16) (result *uint16) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(uint16)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithIP(existing, other net.IP) (result net.IP) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = make(net.IP, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
return other
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
} else if other == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result = new(logging.Level)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeStringSlices(a, b []string) (result []string) {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]struct{}, len(a)+len(b))
|
|
||||||
result = make([]string, 0, len(a)+len(b))
|
|
||||||
for _, s := range a {
|
|
||||||
if _, ok := seen[s]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, s)
|
|
||||||
seen[s] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, s := range b {
|
|
||||||
if _, ok := seen[s]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, s)
|
|
||||||
seen[s] = struct{}{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeUint16Slices(a, b []uint16) (result []uint16) {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[uint16]struct{}, len(a)+len(b))
|
|
||||||
result = make([]uint16, 0, len(a)+len(b))
|
|
||||||
for _, n := range a {
|
|
||||||
if _, ok := seen[n]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, n)
|
|
||||||
seen[n] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, n := range b {
|
|
||||||
if _, ok := seen[n]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, n)
|
|
||||||
seen[n] = struct{}{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeIPNetsSlices(a, b []net.IPNet) (result []net.IPNet) {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]struct{}, len(a)+len(b))
|
|
||||||
result = make([]net.IPNet, 0, len(a)+len(b))
|
|
||||||
for _, ipNet := range a {
|
|
||||||
key := ipNet.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ipNet)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, ipNet := range b {
|
|
||||||
key := ipNet.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ipNet)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeNetaddrIPsSlices(a, b []netaddr.IP) (result []netaddr.IP) {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]struct{}, len(a)+len(b))
|
|
||||||
result = make([]netaddr.IP, 0, len(a)+len(b))
|
|
||||||
for _, ip := range a {
|
|
||||||
key := ip.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ip)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, ip := range b {
|
|
||||||
key := ip.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ip)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeIPPrefixesSlices(a, b []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]struct{}, len(a)+len(b))
|
|
||||||
result = make([]netaddr.IPPrefix, 0, len(a)+len(b))
|
|
||||||
for _, ipPrefix := range a {
|
|
||||||
key := ipPrefix.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ipPrefix)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, ipPrefix := range b {
|
|
||||||
key := ipPrefix.String()
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue // duplicate
|
|
||||||
}
|
|
||||||
result = append(result, ipPrefix)
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ChoicesOrString(choices []string) string {
|
|
||||||
return strings.Join(
|
|
||||||
choices[:len(choices)-1], ", ") +
|
|
||||||
" or " + choices[len(choices)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func PortChoicesOrString(ports []uint16) (s string) {
|
|
||||||
switch len(ports) {
|
|
||||||
case 0:
|
|
||||||
return "there is no allowed port"
|
|
||||||
case 1:
|
|
||||||
return "allowed port is " + fmt.Sprint(ports[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
s = "allowed ports are "
|
|
||||||
portStrings := make([]string, len(ports))
|
|
||||||
for i := range ports {
|
|
||||||
portStrings[i] = fmt.Sprint(ports[i])
|
|
||||||
}
|
|
||||||
s += ChoicesOrString(portStrings)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
func ObfuscateWireguardKey(fullKey string) (obfuscatedKey string) {
|
|
||||||
const minKeyLength = 10
|
|
||||||
if len(fullKey) < minKeyLength {
|
|
||||||
return "(too short)"
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex := len(fullKey) - 1
|
|
||||||
return fullKey[0:2] + "..." + fullKey[lastIndex-2:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ObfuscatePassword(password string) (obfuscatedPassword string) {
|
|
||||||
if password != "" {
|
|
||||||
return "[set]"
|
|
||||||
}
|
|
||||||
return "[not set]"
|
|
||||||
}
|
|
||||||
|
|
||||||
func ObfuscateData(data string) (obfuscated string) {
|
|
||||||
if data != "" {
|
|
||||||
return "[set]"
|
|
||||||
}
|
|
||||||
return "[not set]"
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
"inet.af/netaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func OverrideWithBool(existing, other *bool) (result *bool) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(bool)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithString(existing, other string) (result string) {
|
|
||||||
if other == "" {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
return other
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithStringPtr(existing, other *string) (result *string) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(string)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithInt(existing, other *int) (result *int) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(int)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithUint8(existing, other *uint8) (result *uint8) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(uint8)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithUint16(existing, other *uint16) (result *uint16) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(uint16)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithIP(existing, other net.IP) (result net.IP) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make(net.IP, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(time.Duration)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = new(logging.Level)
|
|
||||||
*result = *other
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithStringSlice(existing, other []string) (result []string) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make([]string, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make([]uint16, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make([]net.IPNet, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make([]netaddr.IP, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
|
|
||||||
if other == nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
result = make([]netaddr.IPPrefix, len(other))
|
|
||||||
copy(result, other)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// StringPtr returns a pointer to the string value
|
|
||||||
// passed as argument.
|
|
||||||
func StringPtr(s string) *string { return &s }
|
|
||||||
|
|
||||||
// DurationPtr returns a pointer to the duration value
|
|
||||||
// passed as argument.
|
|
||||||
func DurationPtr(d time.Duration) *time.Duration { return &d }
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
func BoolPtrToYesNo(b *bool) string {
|
|
||||||
if *b {
|
|
||||||
return "yes"
|
|
||||||
}
|
|
||||||
return "no"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TCPPtrToString(tcp *bool) string {
|
|
||||||
if *tcp {
|
|
||||||
return "TCP"
|
|
||||||
}
|
|
||||||
return "UDP"
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,30 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
func boolPtr(b bool) *bool { return &b }
|
import gomock "github.com/golang/mock/gomock"
|
||||||
func uint8Ptr(n uint8) *uint8 { return &n }
|
|
||||||
|
type sourceKeyValue struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
|
||||||
|
source := NewMockSource(ctrl)
|
||||||
|
var previousCall *gomock.Call
|
||||||
|
for _, keyValue := range keyValues {
|
||||||
|
transformedKey := keyValue.key
|
||||||
|
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
|
||||||
|
if previousCall != nil {
|
||||||
|
keyTransformCall.After(previousCall)
|
||||||
|
}
|
||||||
|
isSet := keyValue.value != ""
|
||||||
|
previousCall = source.EXPECT().Get(transformedKey).
|
||||||
|
Return(keyValue.value, isSet).After(keyTransformCall)
|
||||||
|
if isSet {
|
||||||
|
previousCall = source.EXPECT().KeyTransform(keyValue.key).
|
||||||
|
Return(transformedKey).After(previousCall)
|
||||||
|
previousCall = source.EXPECT().String().
|
||||||
|
Return("mock source").After(previousCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
"github.com/qdm12/govalid/address"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPProxy contains settings to configure the HTTP proxy.
|
// HTTPProxy contains settings to configure the HTTP proxy.
|
||||||
@@ -33,16 +36,19 @@ type HTTPProxy struct {
|
|||||||
// each request/response. It cannot be nil in the
|
// each request/response. It cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
Log *bool
|
Log *bool
|
||||||
|
// ReadHeaderTimeout is the HTTP header read timeout duration
|
||||||
|
// of the HTTP server. It defaults to 1 second if left unset.
|
||||||
|
ReadHeaderTimeout time.Duration
|
||||||
|
// ReadTimeout is the HTTP read timeout duration
|
||||||
|
// of the HTTP server. It defaults to 3 seconds if left unset.
|
||||||
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HTTPProxy) validate() (err error) {
|
func (h HTTPProxy) validate() (err error) {
|
||||||
// Do not validate user and password
|
// Do not validate user and password
|
||||||
|
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
|
||||||
uid := os.Getuid()
|
|
||||||
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s",
|
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
|
||||||
ErrServerAddressNotValid, h.ListeningAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -50,45 +56,42 @@ func (h HTTPProxy) validate() (err error) {
|
|||||||
|
|
||||||
func (h *HTTPProxy) copy() (copied HTTPProxy) {
|
func (h *HTTPProxy) copy() (copied HTTPProxy) {
|
||||||
return HTTPProxy{
|
return HTTPProxy{
|
||||||
User: helpers.CopyStringPtr(h.User),
|
User: gosettings.CopyPointer(h.User),
|
||||||
Password: helpers.CopyStringPtr(h.Password),
|
Password: gosettings.CopyPointer(h.Password),
|
||||||
ListeningAddress: h.ListeningAddress,
|
ListeningAddress: h.ListeningAddress,
|
||||||
Enabled: helpers.CopyBoolPtr(h.Enabled),
|
Enabled: gosettings.CopyPointer(h.Enabled),
|
||||||
Stealth: helpers.CopyBoolPtr(h.Stealth),
|
Stealth: gosettings.CopyPointer(h.Stealth),
|
||||||
Log: helpers.CopyBoolPtr(h.Log),
|
Log: gosettings.CopyPointer(h.Log),
|
||||||
|
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||||
|
ReadTimeout: h.ReadTimeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
|
|
||||||
h.User = helpers.MergeWithStringPtr(h.User, other.User)
|
|
||||||
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
|
|
||||||
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
|
|
||||||
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
|
|
||||||
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
|
|
||||||
h.Log = helpers.MergeWithBool(h.Log, other.Log)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
|
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
|
||||||
h.User = helpers.OverrideWithStringPtr(h.User, other.User)
|
h.User = gosettings.OverrideWithPointer(h.User, other.User)
|
||||||
h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password)
|
h.Password = gosettings.OverrideWithPointer(h.Password, other.Password)
|
||||||
h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress)
|
h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress)
|
||||||
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
|
h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled)
|
||||||
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
|
h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth)
|
||||||
h.Log = helpers.OverrideWithBool(h.Log, other.Log)
|
h.Log = gosettings.OverrideWithPointer(h.Log, other.Log)
|
||||||
|
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||||
|
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPProxy) setDefaults() {
|
func (h *HTTPProxy) setDefaults() {
|
||||||
h.User = helpers.DefaultStringPtr(h.User, "")
|
h.User = gosettings.DefaultPointer(h.User, "")
|
||||||
h.Password = helpers.DefaultStringPtr(h.Password, "")
|
h.Password = gosettings.DefaultPointer(h.Password, "")
|
||||||
h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, ":8888")
|
h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888")
|
||||||
h.Enabled = helpers.DefaultBool(h.Enabled, false)
|
h.Enabled = gosettings.DefaultPointer(h.Enabled, false)
|
||||||
h.Stealth = helpers.DefaultBool(h.Stealth, false)
|
h.Stealth = gosettings.DefaultPointer(h.Stealth, false)
|
||||||
h.Log = helpers.DefaultBool(h.Log, false)
|
h.Log = gosettings.DefaultPointer(h.Log, false)
|
||||||
|
const defaultReadHeaderTimeout = time.Second
|
||||||
|
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||||
|
const defaultReadTimeout = 3 * time.Second
|
||||||
|
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HTTPProxy) String() string {
|
func (h HTTPProxy) String() string {
|
||||||
@@ -97,16 +100,83 @@ func (h HTTPProxy) String() string {
|
|||||||
|
|
||||||
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
|
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("HTTP proxy settings:")
|
node = gotree.New("HTTP proxy settings:")
|
||||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled))
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(h.Enabled))
|
||||||
if !*h.Enabled {
|
if !*h.Enabled {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Appendf("Listening address: %s", h.ListeningAddress)
|
node.Appendf("Listening address: %s", h.ListeningAddress)
|
||||||
node.Appendf("User: %s", *h.User)
|
node.Appendf("User: %s", *h.User)
|
||||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
|
node.Appendf("Password: %s", gosettings.ObfuscateKey(*h.Password))
|
||||||
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
|
node.Appendf("Stealth mode: %s", gosettings.BoolToYesNo(h.Stealth))
|
||||||
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
|
node.Appendf("Log: %s", gosettings.BoolToYesNo(h.Log))
|
||||||
|
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||||
|
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTPProxy) read(r *reader.Reader) (err error) {
|
||||||
|
h.User = r.Get("HTTPPROXY_USER",
|
||||||
|
reader.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
h.Password = r.Get("HTTPPROXY_PASSWORD",
|
||||||
|
reader.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
h.ListeningAddress, err = readHTTProxyListeningAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Enabled, err = r.BoolPtr("HTTPPROXY", reader.RetroKeys("PROXY", "TINYPROXY"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Stealth, err = r.BoolPtr("HTTPPROXY_STEALTH")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Log, err = readHTTProxyLog(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHTTProxyListeningAddress(r *reader.Reader) (listeningAddress string, err error) {
|
||||||
|
// Retro-compatible keys using a port only
|
||||||
|
port, err := r.Uint16Ptr("",
|
||||||
|
reader.RetroKeys("HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT"),
|
||||||
|
reader.IsRetro("HTTPPROXY_LISTENING_ADDRESS"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if port != nil {
|
||||||
|
return fmt.Sprintf(":%d", *port), nil
|
||||||
|
}
|
||||||
|
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
|
||||||
|
return r.String(currentKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
|
||||||
|
const currentKey = "HTTPPROXY_LOG"
|
||||||
|
// Retro-compatible keys using different boolean verbs
|
||||||
|
value := r.String("",
|
||||||
|
reader.RetroKeys("PROXY_LOG", "TINYPROXY_LOG"),
|
||||||
|
reader.IsRetro(currentKey))
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "":
|
||||||
|
return r.BoolPtr(currentKey)
|
||||||
|
case "on", "info", "connect", "notice":
|
||||||
|
return ptrTo(true), nil
|
||||||
|
case "disabled", "no", "off":
|
||||||
|
return ptrTo(false), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
|
||||||
|
ErrValueUnknown, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
5
internal/configuration/settings/interfaces.go
Normal file
5
internal/configuration/settings/interfaces.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
type Warner interface {
|
||||||
|
Warn(message string)
|
||||||
|
}
|
||||||
@@ -1,43 +1,44 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"fmt"
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
|
"github.com/qdm12/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Log contains settings to configure the logger.
|
// Log contains settings to configure the logger.
|
||||||
type Log struct {
|
type Log struct {
|
||||||
// Level is the log level of the logger.
|
// Level is the log level of the logger.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be empty in the internal state.
|
||||||
Level *logging.Level
|
Level string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Log) validate() (err error) {
|
func (l Log) validate() (err error) {
|
||||||
|
_, err = log.ParseLevel(l.Level)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("level: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) copy() (copied Log) {
|
func (l *Log) copy() (copied Log) {
|
||||||
return Log{
|
return Log{
|
||||||
Level: helpers.CopyLogLevelPtr(l.Level),
|
Level: l.Level,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (l *Log) mergeWith(other Log) {
|
|
||||||
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (l *Log) overrideWith(other Log) {
|
func (l *Log) overrideWith(other Log) {
|
||||||
l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
|
l.Level = gosettings.OverrideWithComparable(l.Level, other.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) setDefaults() {
|
func (l *Log) setDefaults() {
|
||||||
l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo)
|
l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Log) String() string {
|
func (l Log) String() string {
|
||||||
@@ -46,6 +47,11 @@ func (l Log) String() string {
|
|||||||
|
|
||||||
func (l Log) toLinesNode() (node *gotree.Node) {
|
func (l Log) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Log settings:")
|
node = gotree.New("Log settings:")
|
||||||
node.Appendf("Log level: %s", l.Level.String())
|
node.Appendf("Log level: %s", l.Level)
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) read(r *reader.Reader) (err error) {
|
||||||
|
l.Level = r.String("LOG_LEVEL")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
4
internal/configuration/settings/mocks_generate_test.go
Normal file
4
internal/configuration/settings/mocks_generate_test.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
|
||||||
|
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source
|
||||||
77
internal/configuration/settings/mocks_reader_test.go
Normal file
77
internal/configuration/settings/mocks_reader_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
|
||||||
|
|
||||||
|
// Package settings is a generated GoMock package.
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockSource is a mock of Source interface.
|
||||||
|
type MockSource struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockSourceMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockSourceMockRecorder is the mock recorder for MockSource.
|
||||||
|
type MockSourceMockRecorder struct {
|
||||||
|
mock *MockSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockSource creates a new mock instance.
|
||||||
|
func NewMockSource(ctrl *gomock.Controller) *MockSource {
|
||||||
|
mock := &MockSource{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockSourceMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mocks base method.
|
||||||
|
func (m *MockSource) Get(arg0 string) (string, bool) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Get", arg0)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
ret1, _ := ret[1].(bool)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get indicates an expected call of Get.
|
||||||
|
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyTransform mocks base method.
|
||||||
|
func (m *MockSource) KeyTransform(arg0 string) string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "KeyTransform", arg0)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyTransform indicates an expected call of KeyTransform.
|
||||||
|
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String mocks base method.
|
||||||
|
func (m *MockSource) String() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "String")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String indicates an expected call of String.
|
||||||
|
func (mr *MockSourceMockRecorder) String() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
|
||||||
|
}
|
||||||
46
internal/configuration/settings/mocks_test.go
Normal file
46
internal/configuration/settings/mocks_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
|
||||||
|
|
||||||
|
// Package settings is a generated GoMock package.
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockWarner is a mock of Warner interface.
|
||||||
|
type MockWarner struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockWarnerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||||
|
type MockWarnerMockRecorder struct {
|
||||||
|
mock *MockWarner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockWarner creates a new mock instance.
|
||||||
|
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||||
|
mock := &MockWarner{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn mocks base method.
|
||||||
|
func (m *MockWarner) Warn(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Warn", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn indicates an expected call of Warn.
|
||||||
|
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||||
|
}
|
||||||
43
internal/configuration/settings/nordvpn_retro.go
Normal file
43
internal/configuration/settings/nordvpn_retro.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
// Retro-compatibility because SERVER_REGIONS changed to SERVER_COUNTRIES
|
||||||
|
// and SERVER_REGIONS is now the continent field for servers.
|
||||||
|
// TODO v4 remove.
|
||||||
|
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
|
||||||
|
updatedSelection ServerSelection,
|
||||||
|
) {
|
||||||
|
validRegionsMap := stringSliceToMap(validRegions)
|
||||||
|
validCountriesMap := stringSliceToMap(validCountries)
|
||||||
|
|
||||||
|
updatedSelection = selection.copy()
|
||||||
|
updatedSelection.Regions = make([]string, 0, len(selection.Regions))
|
||||||
|
for _, region := range selection.Regions {
|
||||||
|
_, isValid := validRegionsMap[region]
|
||||||
|
if isValid {
|
||||||
|
updatedSelection.Regions = append(updatedSelection.Regions, region)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isValid = validCountriesMap[region]
|
||||||
|
if !isValid {
|
||||||
|
// Region is not valid for the country or region
|
||||||
|
// just leave it to the validation to fail it later
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region is not valid for a region, but is a valid country
|
||||||
|
// Handle retro-compatibility and transfer the value to the
|
||||||
|
// country field.
|
||||||
|
updatedSelection.Countries = append(updatedSelection.Countries, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedSelection
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSliceToMap(slice []string) (m map[string]struct{}) {
|
||||||
|
m = make(map[string]struct{}, len(slice))
|
||||||
|
for _, s := range slice {
|
||||||
|
m[s] = struct{}{}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
@@ -1,146 +1,140 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/openvpn/parse"
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenVPN contains settings to configure the OpenVPN client.
|
// OpenVPN contains settings to configure the OpenVPN client.
|
||||||
type OpenVPN struct {
|
type OpenVPN struct {
|
||||||
// Version is the OpenVPN version to run.
|
// Version is the OpenVPN version to run.
|
||||||
// It can only be "2.4" or "2.5".
|
// It can only be "2.5" or "2.6".
|
||||||
Version string
|
Version string `json:"version"`
|
||||||
// User is the OpenVPN authentication username.
|
// User is the OpenVPN authentication username.
|
||||||
// It cannot be an empty string in the internal state
|
// It cannot be nil in the internal state if OpenVPN is used.
|
||||||
// if OpenVPN is used.
|
// It is usually required but in some cases can be the empty string
|
||||||
User string
|
// to indicate no user+password authentication is needed.
|
||||||
|
User *string `json:"user"`
|
||||||
// Password is the OpenVPN authentication password.
|
// Password is the OpenVPN authentication password.
|
||||||
// It cannot be an empty string in the internal state
|
// It cannot be nil in the internal state if OpenVPN is used.
|
||||||
// if OpenVPN is used.
|
// It is usually required but in some cases can be the empty string
|
||||||
Password string
|
// to indicate no user+password authentication is needed.
|
||||||
|
Password *string `json:"password"`
|
||||||
// ConfFile is a custom OpenVPN configuration file path.
|
// ConfFile is a custom OpenVPN configuration file path.
|
||||||
// It can be set to the empty string for it to be ignored.
|
// It can be set to the empty string for it to be ignored.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
ConfFile *string
|
ConfFile *string `json:"config_file_path"`
|
||||||
// Ciphers is a list of ciphers to use for OpenVPN,
|
// Ciphers is a list of ciphers to use for OpenVPN,
|
||||||
// different from the ones specified by the VPN
|
// different from the ones specified by the VPN
|
||||||
// service provider configuration files.
|
// service provider configuration files.
|
||||||
Ciphers []string
|
Ciphers []string `json:"ciphers"`
|
||||||
// Auth is an auth algorithm to use in OpenVPN instead
|
// Auth is an auth algorithm to use in OpenVPN instead
|
||||||
// of the one specified by the VPN service provider.
|
// of the one specified by the VPN service provider.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
// It is ignored if it is set to the empty string.
|
// It is ignored if it is set to the empty string.
|
||||||
Auth *string
|
Auth *string `json:"auth"`
|
||||||
// ClientCrt is the OpenVPN client certificate.
|
// Cert is the base64 encoded DER of an OpenVPN certificate for the <cert> block.
|
||||||
// This is notably used by Cyberghost.
|
// This is notably used by Cyberghost and VPN secure.
|
||||||
// It can be set to the empty string to be ignored.
|
// It can be set to the empty string to be ignored.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
ClientCrt *string
|
Cert *string `json:"cert"`
|
||||||
// ClientKey is the OpenVPN client key.
|
// Key is the base64 encoded DER of an OpenVPN key.
|
||||||
// This is used by Cyberghost and VPN Unlimited.
|
// This is used by Cyberghost and VPN Unlimited.
|
||||||
// It can be set to the empty string to be ignored.
|
// It can be set to the empty string to be ignored.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
ClientKey *string
|
Key *string `json:"key"`
|
||||||
|
// EncryptedKey is the base64 encoded DER of an encrypted key for OpenVPN.
|
||||||
|
// It is used by VPN secure.
|
||||||
|
// It defaults to the empty string meaning it is not
|
||||||
|
// to be used. KeyPassphrase must be set if this one is set.
|
||||||
|
EncryptedKey *string `json:"encrypted_key"`
|
||||||
|
// KeyPassphrase is the key passphrase to be used by OpenVPN
|
||||||
|
// to decrypt the EncryptedPrivateKey. It defaults to the
|
||||||
|
// empty string and must be set if EncryptedPrivateKey is set.
|
||||||
|
KeyPassphrase *string `json:"key_passphrase"`
|
||||||
// PIAEncPreset is the encryption preset for
|
// PIAEncPreset is the encryption preset for
|
||||||
// Private Internet Access. It can be set to an
|
// Private Internet Access. It can be set to an
|
||||||
// empty string for other providers.
|
// empty string for other providers.
|
||||||
PIAEncPreset *string
|
PIAEncPreset *string `json:"pia_encryption_preset"`
|
||||||
// IPv6 is set to true if IPv6 routing should be
|
|
||||||
// set to be tunnel in OpenVPN, and false otherwise.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
IPv6 *bool // TODO automate like with Wireguard
|
|
||||||
// MSSFix is the value (1 to 10000) to set for the
|
// MSSFix is the value (1 to 10000) to set for the
|
||||||
// mssfix option for OpenVPN. It is ignored if set to 0.
|
// mssfix option for OpenVPN. It is ignored if set to 0.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
MSSFix *uint16
|
MSSFix *uint16 `json:"mssfix"`
|
||||||
// Interface is the OpenVPN device interface name.
|
// Interface is the OpenVPN device interface name.
|
||||||
// It cannot be an empty string in the internal state.
|
// It cannot be an empty string in the internal state.
|
||||||
Interface string
|
Interface string `json:"interface"`
|
||||||
// Root is true if OpenVPN is to be run as root,
|
// ProcessUser is the OpenVPN process OS username
|
||||||
// and false otherwise. It cannot be nil in the
|
// to use. It cannot be empty in the internal state.
|
||||||
// internal state.
|
// It defaults to 'root'.
|
||||||
Root *bool
|
ProcessUser string `json:"process_user"`
|
||||||
// ProcUser is the OpenVPN process OS username
|
|
||||||
// to use. It cannot be nil in the internal state.
|
|
||||||
// This is set and injected at runtime.
|
|
||||||
// TODO only use ProcUser and not Root field.
|
|
||||||
ProcUser string
|
|
||||||
// Verbosity is the OpenVPN verbosity level from 0 to 6.
|
// Verbosity is the OpenVPN verbosity level from 0 to 6.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
Verbosity *int
|
Verbosity *int `json:"verbosity"`
|
||||||
// Flags is a slice of additional flags to be passed
|
// Flags is a slice of additional flags to be passed
|
||||||
// to the OpenVPN program.
|
// to the OpenVPN program.
|
||||||
Flags []string
|
Flags []string `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}$`)
|
||||||
|
|
||||||
func (o OpenVPN) validate(vpnProvider string) (err error) {
|
func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||||
// Validate version
|
// Validate version
|
||||||
validVersions := []string{constants.Openvpn24, constants.Openvpn25}
|
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
|
||||||
if !helpers.IsOneOf(o.Version, validVersions...) {
|
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
|
||||||
return fmt.Errorf("%w: %q can only be one of %s",
|
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
|
||||||
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isCustom := vpnProvider == constants.Custom
|
isCustom := vpnProvider == providers.Custom
|
||||||
|
isUserRequired := !isCustom &&
|
||||||
|
vpnProvider != providers.Airvpn &&
|
||||||
|
vpnProvider != providers.VPNSecure
|
||||||
|
|
||||||
if !isCustom && o.User == "" {
|
if isUserRequired && *o.User == "" {
|
||||||
return ErrOpenVPNUserIsEmpty
|
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isCustom && o.Password == "" {
|
passwordRequired := isUserRequired &&
|
||||||
return ErrOpenVPNPasswordIsEmpty
|
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
|
||||||
|
|
||||||
|
if passwordRequired && *o.Password == "" {
|
||||||
|
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate ConfFile
|
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
|
||||||
if isCustom {
|
if err != nil {
|
||||||
if *o.ConfFile == "" {
|
return fmt.Errorf("custom configuration file: %w", err)
|
||||||
return fmt.Errorf("%w: no file path specified", ErrOpenVPNConfigFile)
|
|
||||||
}
|
|
||||||
err := helpers.FileExists(*o.ConfFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check client certificate
|
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert)
|
||||||
switch vpnProvider {
|
if err != nil {
|
||||||
case
|
return fmt.Errorf("client certificate: %w", err)
|
||||||
constants.Cyberghost,
|
|
||||||
constants.VPNUnlimited:
|
|
||||||
if *o.ClientCrt == "" {
|
|
||||||
return ErrOpenVPNClientCertMissing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if *o.ClientCrt != "" {
|
|
||||||
_, err = parse.ExtractCert([]byte(*o.ClientCrt))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrOpenVPNClientCertNotValid, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check client key
|
err = validateOpenVPNClientKey(vpnProvider, *o.Key)
|
||||||
switch vpnProvider {
|
if err != nil {
|
||||||
case
|
return fmt.Errorf("client key: %w", err)
|
||||||
constants.Cyberghost,
|
}
|
||||||
constants.VPNUnlimited,
|
|
||||||
constants.Wevpn:
|
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
|
||||||
if *o.ClientKey == "" {
|
if err != nil {
|
||||||
return ErrOpenVPNClientKeyMissing
|
return fmt.Errorf("encrypted key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if *o.ClientKey != "" {
|
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
|
||||||
_, err = parse.ExtractPrivateKey([]byte(*o.ClientKey))
|
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrOpenVPNClientKeyNotValid, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate MSSFix
|
|
||||||
const maxMSSFix = 10000
|
const maxMSSFix = 10000
|
||||||
if *o.MSSFix > maxMSSFix {
|
if *o.MSSFix > maxMSSFix {
|
||||||
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
||||||
@@ -152,7 +146,6 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
|||||||
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
|
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Verbosity
|
|
||||||
if *o.Verbosity < 0 || *o.Verbosity > 6 {
|
if *o.Verbosity < 0 || *o.Verbosity > 6 {
|
||||||
return fmt.Errorf("%w: %d can only be between 0 and 5",
|
return fmt.Errorf("%w: %d can only be between 0 and 5",
|
||||||
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
|
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
|
||||||
@@ -161,93 +154,165 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPN) copy() (copied OpenVPN) {
|
func validateOpenVPNConfigFilepath(isCustom bool,
|
||||||
return OpenVPN{
|
confFile string,
|
||||||
Version: o.Version,
|
) (err error) {
|
||||||
User: o.User,
|
if !isCustom {
|
||||||
Password: o.Password,
|
return nil
|
||||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
|
||||||
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
|
||||||
Auth: helpers.CopyStringPtr(o.Auth),
|
|
||||||
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
|
|
||||||
ClientKey: helpers.CopyStringPtr(o.ClientKey),
|
|
||||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
|
||||||
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
|
||||||
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
|
||||||
Interface: o.Interface,
|
|
||||||
Root: helpers.CopyBoolPtr(o.Root),
|
|
||||||
ProcUser: o.ProcUser,
|
|
||||||
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
|
||||||
Flags: helpers.CopyStringSlice(o.Flags),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if confFile == "" {
|
||||||
|
return fmt.Errorf("%w", ErrFilepathMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validate.FileExists(confFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
extractor := extract.New()
|
||||||
|
_, _, err = extractor.Data(confFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("extracting information from custom configuration file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
func validateOpenVPNClientCertificate(vpnProvider,
|
||||||
// unset field of the receiver settings object.
|
clientCert string,
|
||||||
func (o *OpenVPN) mergeWith(other OpenVPN) {
|
) (err error) {
|
||||||
o.Version = helpers.MergeWithString(o.Version, other.Version)
|
switch vpnProvider {
|
||||||
o.User = helpers.MergeWithString(o.User, other.User)
|
case
|
||||||
o.Password = helpers.MergeWithString(o.Password, other.Password)
|
providers.Airvpn,
|
||||||
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
|
providers.Cyberghost,
|
||||||
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
|
providers.VPNSecure,
|
||||||
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
|
providers.VPNUnlimited:
|
||||||
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
|
if clientCert == "" {
|
||||||
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
}
|
||||||
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
|
}
|
||||||
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
|
|
||||||
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
|
if clientCert == "" {
|
||||||
o.Root = helpers.MergeWithBool(o.Root, other.Root)
|
return nil
|
||||||
o.ProcUser = helpers.MergeWithString(o.ProcUser, other.ProcUser)
|
}
|
||||||
o.Verbosity = helpers.MergeWithInt(o.Verbosity, other.Verbosity)
|
|
||||||
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
|
_, err = base64.StdEncoding.DecodeString(clientCert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
||||||
|
switch vpnProvider {
|
||||||
|
case
|
||||||
|
providers.Airvpn,
|
||||||
|
providers.Cyberghost,
|
||||||
|
providers.VPNUnlimited,
|
||||||
|
providers.Wevpn:
|
||||||
|
if clientKey == "" {
|
||||||
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = base64.StdEncoding.DecodeString(clientKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||||
|
encryptedPrivateKey string,
|
||||||
|
) (err error) {
|
||||||
|
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||||
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encryptedPrivateKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = base64.StdEncoding.DecodeString(encryptedPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPN) copy() (copied OpenVPN) {
|
||||||
|
return OpenVPN{
|
||||||
|
Version: o.Version,
|
||||||
|
User: gosettings.CopyPointer(o.User),
|
||||||
|
Password: gosettings.CopyPointer(o.Password),
|
||||||
|
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||||
|
Ciphers: gosettings.CopySlice(o.Ciphers),
|
||||||
|
Auth: gosettings.CopyPointer(o.Auth),
|
||||||
|
Cert: gosettings.CopyPointer(o.Cert),
|
||||||
|
Key: gosettings.CopyPointer(o.Key),
|
||||||
|
EncryptedKey: gosettings.CopyPointer(o.EncryptedKey),
|
||||||
|
KeyPassphrase: gosettings.CopyPointer(o.KeyPassphrase),
|
||||||
|
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||||
|
MSSFix: gosettings.CopyPointer(o.MSSFix),
|
||||||
|
Interface: o.Interface,
|
||||||
|
ProcessUser: o.ProcessUser,
|
||||||
|
Verbosity: gosettings.CopyPointer(o.Verbosity),
|
||||||
|
Flags: gosettings.CopySlice(o.Flags),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (o *OpenVPN) overrideWith(other OpenVPN) {
|
func (o *OpenVPN) overrideWith(other OpenVPN) {
|
||||||
o.Version = helpers.OverrideWithString(o.Version, other.Version)
|
o.Version = gosettings.OverrideWithComparable(o.Version, other.Version)
|
||||||
o.User = helpers.OverrideWithString(o.User, other.User)
|
o.User = gosettings.OverrideWithPointer(o.User, other.User)
|
||||||
o.Password = helpers.OverrideWithString(o.Password, other.Password)
|
o.Password = gosettings.OverrideWithPointer(o.Password, other.Password)
|
||||||
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
|
o.Ciphers = gosettings.OverrideWithSlice(o.Ciphers, other.Ciphers)
|
||||||
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
|
o.Auth = gosettings.OverrideWithPointer(o.Auth, other.Auth)
|
||||||
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
|
o.Cert = gosettings.OverrideWithPointer(o.Cert, other.Cert)
|
||||||
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
|
o.Key = gosettings.OverrideWithPointer(o.Key, other.Key)
|
||||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
o.EncryptedKey = gosettings.OverrideWithPointer(o.EncryptedKey, other.EncryptedKey)
|
||||||
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
|
o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase)
|
||||||
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
o.Interface = helpers.OverrideWithString(o.Interface, other.Interface)
|
o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix)
|
||||||
o.Root = helpers.OverrideWithBool(o.Root, other.Root)
|
o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface)
|
||||||
o.ProcUser = helpers.OverrideWithString(o.ProcUser, other.ProcUser)
|
o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser)
|
||||||
o.Verbosity = helpers.OverrideWithInt(o.Verbosity, other.Verbosity)
|
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity)
|
||||||
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
|
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPN) setDefaults(vpnProvider string) {
|
func (o *OpenVPN) setDefaults(vpnProvider string) {
|
||||||
o.Version = helpers.DefaultString(o.Version, constants.Openvpn25)
|
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26)
|
||||||
if vpnProvider == constants.Mullvad {
|
o.User = gosettings.DefaultPointer(o.User, "")
|
||||||
o.Password = "m"
|
if vpnProvider == providers.Mullvad {
|
||||||
|
o.Password = gosettings.DefaultPointer(o.Password, "m")
|
||||||
|
} else {
|
||||||
|
o.Password = gosettings.DefaultPointer(o.Password, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
|
o.Auth = gosettings.DefaultPointer(o.Auth, "")
|
||||||
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
|
o.Cert = gosettings.DefaultPointer(o.Cert, "")
|
||||||
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
|
o.Key = gosettings.DefaultPointer(o.Key, "")
|
||||||
|
o.EncryptedKey = gosettings.DefaultPointer(o.EncryptedKey, "")
|
||||||
|
o.KeyPassphrase = gosettings.DefaultPointer(o.KeyPassphrase, "")
|
||||||
|
|
||||||
var defaultEncPreset string
|
var defaultEncPreset string
|
||||||
if vpnProvider == constants.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
defaultEncPreset = constants.PIAEncryptionPresetStrong
|
defaultEncPreset = presets.Strong
|
||||||
}
|
}
|
||||||
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
|
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
|
||||||
|
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
|
||||||
o.IPv6 = helpers.DefaultBool(o.IPv6, false)
|
o.Interface = gosettings.DefaultComparable(o.Interface, "tun0")
|
||||||
o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0)
|
o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root")
|
||||||
o.Interface = helpers.DefaultString(o.Interface, "tun0")
|
o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1)
|
||||||
o.Root = helpers.DefaultBool(o.Root, true)
|
|
||||||
o.ProcUser = helpers.DefaultString(o.ProcUser, "root")
|
|
||||||
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OpenVPN) String() string {
|
func (o OpenVPN) String() string {
|
||||||
@@ -257,8 +322,8 @@ func (o OpenVPN) String() string {
|
|||||||
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("OpenVPN settings:")
|
node = gotree.New("OpenVPN settings:")
|
||||||
node.Appendf("OpenVPN version: %s", o.Version)
|
node.Appendf("OpenVPN version: %s", o.Version)
|
||||||
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
|
node.Appendf("User: %s", gosettings.ObfuscateKey(*o.User))
|
||||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
|
node.Appendf("Password: %s", gosettings.ObfuscateKey(*o.Password))
|
||||||
|
|
||||||
if *o.ConfFile != "" {
|
if *o.ConfFile != "" {
|
||||||
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||||
@@ -272,20 +337,23 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Auth: %s", *o.Auth)
|
node.Appendf("Auth: %s", *o.Auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *o.ClientCrt != "" {
|
if *o.Cert != "" {
|
||||||
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
|
node.Appendf("Client crt: %s", gosettings.ObfuscateKey(*o.Cert))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *o.ClientKey != "" {
|
if *o.Key != "" {
|
||||||
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
|
node.Appendf("Client key: %s", gosettings.ObfuscateKey(*o.Key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.EncryptedKey != "" {
|
||||||
|
node.Appendf("Encrypted key: %s (key passhrapse %s)",
|
||||||
|
gosettings.ObfuscateKey(*o.EncryptedKey), gosettings.ObfuscateKey(*o.KeyPassphrase))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *o.PIAEncPreset != "" {
|
if *o.PIAEncPreset != "" {
|
||||||
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6))
|
|
||||||
|
|
||||||
if *o.MSSFix > 0 {
|
if *o.MSSFix > 0 {
|
||||||
node.Appendf("MSS Fix: %d", *o.MSSFix)
|
node.Appendf("MSS Fix: %d", *o.MSSFix)
|
||||||
}
|
}
|
||||||
@@ -294,14 +362,7 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Network interface: %s", o.Interface)
|
node.Appendf("Network interface: %s", o.Interface)
|
||||||
}
|
}
|
||||||
|
|
||||||
processUser := "root"
|
node.Appendf("Run OpenVPN as: %s", o.ProcessUser)
|
||||||
if !*o.Root {
|
|
||||||
processUser = "some non root user" // TODO
|
|
||||||
if o.ProcUser != "" {
|
|
||||||
processUser = o.ProcUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.Appendf("Run OpenVPN as: %s", processUser)
|
|
||||||
|
|
||||||
node.Appendf("Verbosity level: %d", *o.Verbosity)
|
node.Appendf("Verbosity level: %d", *o.Verbosity)
|
||||||
|
|
||||||
@@ -318,3 +379,58 @@ func (o OpenVPN) WithDefaults(provider string) OpenVPN {
|
|||||||
o.setDefaults(provider)
|
o.setDefaults(provider)
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPN) read(r *reader.Reader) (err error) {
|
||||||
|
o.Version = r.String("OPENVPN_VERSION")
|
||||||
|
o.User = r.Get("OPENVPN_USER", reader.RetroKeys("USER"), reader.ForceLowercase(false))
|
||||||
|
o.Password = r.Get("OPENVPN_PASSWORD", reader.RetroKeys("PASSWORD"), reader.ForceLowercase(false))
|
||||||
|
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
||||||
|
o.Ciphers = r.CSV("OPENVPN_CIPHERS", reader.RetroKeys("OPENVPN_CIPHER"))
|
||||||
|
o.Auth = r.Get("OPENVPN_AUTH")
|
||||||
|
o.Cert = r.Get("OPENVPN_CERT", reader.ForceLowercase(false))
|
||||||
|
o.Key = r.Get("OPENVPN_KEY", reader.ForceLowercase(false))
|
||||||
|
o.EncryptedKey = r.Get("OPENVPN_ENCRYPTED_KEY", reader.ForceLowercase(false))
|
||||||
|
o.KeyPassphrase = r.Get("OPENVPN_KEY_PASSPHRASE", reader.ForceLowercase(false))
|
||||||
|
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
|
||||||
|
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
|
||||||
|
|
||||||
|
o.MSSFix, err = r.Uint16Ptr("OPENVPN_MSSFIX")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Interface = r.String("VPN_INTERFACE",
|
||||||
|
reader.RetroKeys("OPENVPN_INTERFACE"), reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
o.ProcessUser, err = readOpenVPNProcessUser(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Verbosity, err = r.IntPtr("OPENVPN_VERBOSITY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
flagsPtr := r.Get("OPENVPN_FLAGS", reader.ForceLowercase(false))
|
||||||
|
if flagsPtr != nil {
|
||||||
|
o.Flags = strings.Fields(*flagsPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOpenVPNProcessUser(r *reader.Reader) (processUser string, err error) {
|
||||||
|
value, err := r.BoolPtr("OPENVPN_ROOT") // Retro-compatibility
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if value != nil {
|
||||||
|
if *value {
|
||||||
|
return "root", nil
|
||||||
|
}
|
||||||
|
const defaultNonRootUser = "nonrootuser"
|
||||||
|
return defaultNonRootUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.String("OPENVPN_PROCESS_USER"), nil
|
||||||
|
}
|
||||||
|
|||||||
43
internal/configuration/settings/openvpn_test.go
Normal file
43
internal/configuration/settings/openvpn_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ivpnAccountID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
match bool
|
||||||
|
}{
|
||||||
|
{},
|
||||||
|
{s: "abc"},
|
||||||
|
{s: "i"},
|
||||||
|
{s: "ivpn"},
|
||||||
|
{s: "ivpn-aaaa"},
|
||||||
|
{s: "ivpn-aaaa-aaaa"},
|
||||||
|
{s: "ivpn-aaaa-aaaa-aaa"},
|
||||||
|
{s: "ivpn-aaaa-aaaa-aaaa", match: true},
|
||||||
|
{s: "ivpn-aaaa-aaaa-aaaaa"},
|
||||||
|
{s: "ivpn-a6B7-fP91-Zh6Y", match: true},
|
||||||
|
{s: "i-aaaa"},
|
||||||
|
{s: "i-aaaa-aaaa"},
|
||||||
|
{s: "i-aaaa-aaaa-aaa"},
|
||||||
|
{s: "i-aaaa-aaaa-aaaa", match: true},
|
||||||
|
{s: "i-aaaa-aaaa-aaaaa"},
|
||||||
|
{s: "i-a6B7-fP91-Zh6Y", match: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.s, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
match := ivpnAccountID.MatchString(testCase.s)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.match, match)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,15 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,35 +19,42 @@ type OpenVPNSelection struct {
|
|||||||
// It can be set to an empty string to indicate to
|
// It can be set to an empty string to indicate to
|
||||||
// NOT use a custom configuration file.
|
// NOT use a custom configuration file.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
ConfFile *string
|
ConfFile *string `json:"config_file_path"`
|
||||||
// TCP is true if the OpenVPN protocol is TCP,
|
// Protocol is the OpenVPN network protocol to use,
|
||||||
// and false for UDP.
|
// and can be udp or tcp. It cannot be the empty string
|
||||||
// It cannot be nil in the internal state.
|
// in the internal state.
|
||||||
TCP *bool
|
Protocol string `json:"protocol"`
|
||||||
// CustomPort is the OpenVPN server endpoint port.
|
// CustomPort is the OpenVPN server endpoint port.
|
||||||
// It can be set to 0 to indicate no custom port should
|
// It can be set to 0 to indicate no custom port should
|
||||||
// be used. It cannot be nil in the internal state.
|
// be used. It cannot be nil in the internal state.
|
||||||
CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
|
CustomPort *uint16 `json:"custom_port"`
|
||||||
// PIAEncPreset is the encryption preset for
|
// PIAEncPreset is the encryption preset for
|
||||||
// Private Internet Access. It can be set to an
|
// Private Internet Access. It can be set to an
|
||||||
// empty string for other providers.
|
// empty string for other providers.
|
||||||
PIAEncPreset *string
|
PIAEncPreset *string `json:"pia_encryption_preset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||||
// Validate ConfFile
|
// Validate ConfFile
|
||||||
if confFile := *o.ConfFile; confFile != "" {
|
if confFile := *o.ConfFile; confFile != "" {
|
||||||
err := helpers.FileExists(confFile)
|
err := validate.FileExists(confFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
|
return fmt.Errorf("configuration file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = validate.IsOneOf(o.Protocol, constants.UDP, constants.TCP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("network protocol: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Validate TCP
|
// Validate TCP
|
||||||
if *o.TCP && helpers.IsOneOf(vpnProvider,
|
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
||||||
constants.Perfectprivacy,
|
providers.Giganews,
|
||||||
constants.Privado,
|
providers.Ipvanish,
|
||||||
constants.Vyprvpn,
|
providers.Perfectprivacy,
|
||||||
|
providers.Privado,
|
||||||
|
providers.Vyprvpn,
|
||||||
) {
|
) {
|
||||||
return fmt.Errorf("%w: for VPN service provider %s",
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
ErrOpenVPNTCPNotSupported, vpnProvider)
|
ErrOpenVPNTCPNotSupported, vpnProvider)
|
||||||
@@ -51,60 +64,74 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
if *o.CustomPort != 0 {
|
if *o.CustomPort != 0 {
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
// no restriction on port
|
// no restriction on port
|
||||||
case constants.Cyberghost, constants.HideMyAss,
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
constants.PrivateInternetAccess, constants.Privatevpn,
|
providers.Privatevpn, providers.Torguard:
|
||||||
constants.Protonvpn, constants.Torguard:
|
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case constants.Expressvpn, constants.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
constants.Ipvanish, constants.Nordvpn,
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
constants.Privado, constants.Purevpn,
|
providers.Privado, providers.Purevpn,
|
||||||
constants.Surfshark, constants.VPNUnlimited,
|
providers.Surfshark, providers.VPNSecure,
|
||||||
constants.Vyprvpn:
|
providers.VPNUnlimited, providers.Vyprvpn:
|
||||||
return fmt.Errorf("%w: for VPN service provider %s",
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
||||||
default:
|
default:
|
||||||
var allowedTCP, allowedUDP []uint16
|
var allowedTCP, allowedUDP []uint16
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case constants.Ivpn:
|
case providers.Airvpn:
|
||||||
|
allowedTCP = []uint16{
|
||||||
|
53, 80, 443, // IP in 1, 3
|
||||||
|
1194, 2018, 41185, // IP in 1, 2, 3, 4
|
||||||
|
}
|
||||||
|
allowedUDP = []uint16{53, 80, 443, 1194, 2018, 41185}
|
||||||
|
case providers.Ivpn:
|
||||||
allowedTCP = []uint16{80, 443, 1143}
|
allowedTCP = []uint16{80, 443, 1143}
|
||||||
allowedUDP = []uint16{53, 1194, 2049, 2050}
|
allowedUDP = []uint16{53, 1194, 2049, 2050}
|
||||||
case constants.Mullvad:
|
case providers.Mullvad:
|
||||||
allowedTCP = []uint16{80, 443, 1401}
|
allowedTCP = []uint16{80, 443, 1401}
|
||||||
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
|
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
|
||||||
case constants.Perfectprivacy:
|
case providers.Perfectprivacy:
|
||||||
allowedTCP = []uint16{44, 443, 4433}
|
allowedTCP = []uint16{44, 443, 4433}
|
||||||
allowedUDP = []uint16{44, 443, 4433}
|
allowedUDP = []uint16{44, 443, 4433}
|
||||||
case constants.Wevpn:
|
case providers.PrivateInternetAccess:
|
||||||
|
allowedTCP = []uint16{80, 110, 443}
|
||||||
|
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
|
||||||
|
case providers.Protonvpn:
|
||||||
|
allowedTCP = []uint16{443, 5995, 8443}
|
||||||
|
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
|
||||||
|
case providers.SlickVPN:
|
||||||
|
allowedTCP = []uint16{443, 8080, 8888}
|
||||||
|
allowedUDP = []uint16{443, 8080, 8888}
|
||||||
|
case providers.Wevpn:
|
||||||
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||||
allowedUDP = []uint16{80, 1194, 1198}
|
allowedUDP = []uint16{80, 1194, 1198}
|
||||||
case constants.Windscribe:
|
case providers.Windscribe:
|
||||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) {
|
allowedPorts := allowedUDP
|
||||||
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
|
if o.Protocol == constants.TCP {
|
||||||
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
|
allowedPorts = allowedTCP
|
||||||
helpers.PortChoicesOrString(allowedTCP))
|
}
|
||||||
} else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) {
|
err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
|
||||||
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
|
if err != nil {
|
||||||
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
|
return fmt.Errorf("%w: for VPN service provider %s: %w",
|
||||||
helpers.PortChoicesOrString(allowedUDP))
|
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate EncPreset
|
// Validate EncPreset
|
||||||
if vpnProvider == constants.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
validEncryptionPresets := []string{
|
validEncryptionPresets := []string{
|
||||||
constants.PIAEncryptionPresetNone,
|
presets.None,
|
||||||
constants.PIAEncryptionPresetNormal,
|
presets.Normal,
|
||||||
constants.PIAEncryptionPresetStrong,
|
presets.Strong,
|
||||||
}
|
}
|
||||||
if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) {
|
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
|
||||||
return fmt.Errorf("%w: %s; valid presets are %s",
|
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
|
||||||
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
|
|
||||||
helpers.ChoicesOrString(validEncryptionPresets))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,37 +140,30 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
|
|
||||||
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
||||||
return OpenVPNSelection{
|
return OpenVPNSelection{
|
||||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||||
TCP: helpers.CopyBoolPtr(o.TCP),
|
Protocol: o.Protocol,
|
||||||
CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
|
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
|
|
||||||
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
|
|
||||||
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
|
|
||||||
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
|
|
||||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
||||||
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP)
|
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||||
o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort)
|
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||||
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
o.TCP = helpers.DefaultBool(o.TCP, false)
|
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||||
o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0)
|
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||||
|
|
||||||
var defaultEncPreset string
|
var defaultEncPreset string
|
||||||
if vpnProvider == constants.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
defaultEncPreset = constants.PIAEncryptionPresetStrong
|
defaultEncPreset = presets.Strong
|
||||||
}
|
}
|
||||||
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
|
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OpenVPNSelection) String() string {
|
func (o OpenVPNSelection) String() string {
|
||||||
@@ -152,7 +172,7 @@ func (o OpenVPNSelection) String() string {
|
|||||||
|
|
||||||
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("OpenVPN server selection settings:")
|
node = gotree.New("OpenVPN server selection settings:")
|
||||||
node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP))
|
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||||
|
|
||||||
if *o.CustomPort != 0 {
|
if *o.CustomPort != 0 {
|
||||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||||
@@ -168,3 +188,20 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
||||||
|
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||||
|
|
||||||
|
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||||
|
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
|
||||||
|
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,57 +15,109 @@ import (
|
|||||||
type PortForwarding struct {
|
type PortForwarding struct {
|
||||||
// Enabled is true if port forwarding should be activated.
|
// Enabled is true if port forwarding should be activated.
|
||||||
// It cannot be nil for the internal state.
|
// It cannot be nil for the internal state.
|
||||||
Enabled *bool
|
Enabled *bool `json:"enabled"`
|
||||||
|
// Provider is set to specify which custom port forwarding code
|
||||||
|
// should be used. This is especially necessary for the custom
|
||||||
|
// provider using Wireguard for a provider where Wireguard is not
|
||||||
|
// natively supported but custom port forwarding code is available.
|
||||||
|
// It defaults to the empty string, meaning the current provider
|
||||||
|
// should be the one used for port forwarding.
|
||||||
|
// It cannot be nil for the internal state.
|
||||||
|
Provider *string `json:"provider"`
|
||||||
// Filepath is the port forwarding status file path
|
// Filepath is the port forwarding status file path
|
||||||
// to use. It can be the empty string to indicate not
|
// to use. It can be the empty string to indicate not
|
||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
Filepath *string
|
Filepath *string `json:"status_file_path"`
|
||||||
|
// UpCommand is the command to use when the port forwarding is up.
|
||||||
|
// It can be the empty string to indicate not to run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
UpCommand *string `json:"up_command"`
|
||||||
|
// DownCommand is the command to use after the port forwarding goes down.
|
||||||
|
// It can be the empty string to indicate to NOT run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
DownCommand *string `json:"down_command"`
|
||||||
|
// ListeningPort is the port traffic would be redirected to from the
|
||||||
|
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||||
|
// is its default as well.
|
||||||
|
ListeningPort *uint16 `json:"listening_port"`
|
||||||
|
// Username is only used for Private Internet Access port forwarding.
|
||||||
|
Username string `json:"username"`
|
||||||
|
// Password is only used for Private Internet Access port forwarding.
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PortForwarding) validate(vpnProvider string) (err error) {
|
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||||
if !*p.Enabled {
|
if !*p.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Enabled
|
// Validate current provider or custom provider specified
|
||||||
validProviders := []string{constants.PrivateInternetAccess}
|
providerSelected := vpnProvider
|
||||||
if !helpers.IsOneOf(vpnProvider, validProviders...) {
|
if *p.Provider != "" {
|
||||||
return fmt.Errorf("%w: for provider %s, it is only available for %s",
|
providerSelected = *p.Provider
|
||||||
ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", "))
|
}
|
||||||
|
validProviders := []string{
|
||||||
|
providers.Perfectprivacy,
|
||||||
|
providers.PrivateInternetAccess,
|
||||||
|
providers.Privatevpn,
|
||||||
|
providers.Protonvpn,
|
||||||
|
}
|
||||||
|
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Filepath
|
// Validate Filepath
|
||||||
if *p.Filepath != "" { // optional
|
if *p.Filepath != "" { // optional
|
||||||
_, err := filepath.Abs(*p.Filepath)
|
_, err := filepath.Abs(*p.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrPortForwardingFilepathNotValid, err)
|
return fmt.Errorf("filepath is not valid: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if providerSelected == providers.PrivateInternetAccess {
|
||||||
|
switch {
|
||||||
|
case p.Username == "":
|
||||||
|
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
|
||||||
|
case p.Password == "":
|
||||||
|
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) copy() (copied PortForwarding) {
|
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||||
return PortForwarding{
|
return PortForwarding{
|
||||||
Enabled: helpers.CopyBoolPtr(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
Filepath: helpers.CopyStringPtr(p.Filepath),
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
|
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||||
|
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||||
|
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||||
|
Username: p.Username,
|
||||||
|
Password: p.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) mergeWith(other PortForwarding) {
|
func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||||
p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
}
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
|
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||||
func (p *PortForwarding) overrideWith(other PortForwarding) {
|
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||||
p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
|
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||||
p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
|
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
|
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) setDefaults() {
|
func (p *PortForwarding) setDefaults() {
|
||||||
p.Enabled = helpers.DefaultBool(p.Enabled, false)
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port")
|
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||||
|
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
|
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||||
|
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||||
|
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PortForwarding) String() string {
|
func (p PortForwarding) String() string {
|
||||||
@@ -77,7 +130,18 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node = gotree.New("Automatic port forwarding settings:")
|
node = gotree.New("Automatic port forwarding settings:")
|
||||||
node.Appendf("Enabled: yes")
|
|
||||||
|
listeningPort := "disabled"
|
||||||
|
if *p.ListeningPort != 0 {
|
||||||
|
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
||||||
|
}
|
||||||
|
node.Appendf("Redirection listening port: %s", listeningPort)
|
||||||
|
|
||||||
|
if *p.Provider == "" {
|
||||||
|
node.Appendf("Use port forwarding code for current provider")
|
||||||
|
} else {
|
||||||
|
node.Appendf("Use code for provider: %s", *p.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
filepath := *p.Filepath
|
filepath := *p.Filepath
|
||||||
if filepath == "" {
|
if filepath == "" {
|
||||||
@@ -85,5 +149,67 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
node.Appendf("Forwarded port file path: %s", filepath)
|
node.Appendf("Forwarded port file path: %s", filepath)
|
||||||
|
|
||||||
|
if *p.UpCommand != "" {
|
||||||
|
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
||||||
|
}
|
||||||
|
if *p.DownCommand != "" {
|
||||||
|
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Username != "" {
|
||||||
|
credentialsNode := node.Appendf("Credentials:")
|
||||||
|
credentialsNode.Appendf("Username: %s", p.Username)
|
||||||
|
credentialsNode.Appendf("Password: %s", gosettings.ObfuscateKey(p.Password))
|
||||||
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
||||||
|
p.Enabled, err = r.BoolPtr("VPN_PORT_FORWARDING",
|
||||||
|
reader.RetroKeys(
|
||||||
|
"PORT_FORWARDING",
|
||||||
|
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Provider = r.Get("VPN_PORT_FORWARDING_PROVIDER")
|
||||||
|
|
||||||
|
p.Filepath = r.Get("VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
|
reader.ForceLowercase(false),
|
||||||
|
reader.RetroKeys(
|
||||||
|
"PORT_FORWARDING_STATUS_FILE",
|
||||||
|
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
|
))
|
||||||
|
|
||||||
|
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameKeys := []string{"VPN_PORT_FORWARDING_USERNAME", "OPENVPN_USER", "USER"}
|
||||||
|
for _, key := range usernameKeys {
|
||||||
|
p.Username = r.String(key, reader.ForceLowercase(false))
|
||||||
|
if p.Username != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordKeys := []string{"VPN_PORT_FORWARDING_PASSWORD", "OPENVPN_PASSWORD", "PASSWORD"}
|
||||||
|
for _, key := range passwordKeys {
|
||||||
|
p.Password = r.String(key, reader.ForceLowercase(false))
|
||||||
|
if p.Password != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
settings := PortForwarding{
|
settings := PortForwarding{
|
||||||
Enabled: boolPtr(false),
|
Enabled: ptrTo(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
s := settings.String()
|
s := settings.String()
|
||||||
|
|||||||
@@ -2,53 +2,60 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider contains settings specific to a VPN provider.
|
// Provider contains settings specific to a VPN provider.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
// Name is the VPN service provider name.
|
// Name is the VPN service provider name.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
Name *string
|
Name string `json:"name"`
|
||||||
// ServerSelection is the settings to
|
// ServerSelection is the settings to
|
||||||
// select the VPN server.
|
// select the VPN server.
|
||||||
ServerSelection ServerSelection
|
ServerSelection ServerSelection `json:"server_selection"`
|
||||||
// PortForwarding is the settings about port forwarding.
|
// PortForwarding is the settings about port forwarding.
|
||||||
PortForwarding PortForwarding
|
PortForwarding PortForwarding `json:"port_forwarding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
func (p *Provider) validate(vpnType string, allServers models.AllServers) (err error) {
|
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||||
// Validate Name
|
// Validate Name
|
||||||
var validNames []string
|
var validNames []string
|
||||||
if vpnType == constants.OpenVPN {
|
if vpnType == vpn.OpenVPN {
|
||||||
validNames = constants.AllProviders()
|
validNames = providers.AllWithCustom()
|
||||||
validNames = append(validNames, "pia") // Retro-compatibility
|
validNames = append(validNames, "pia") // Retro-compatibility
|
||||||
} else { // Wireguard
|
} else { // Wireguard
|
||||||
validNames = []string{
|
validNames = []string{
|
||||||
constants.Custom,
|
providers.Airvpn,
|
||||||
constants.Ivpn,
|
providers.Custom,
|
||||||
constants.Mullvad,
|
providers.Fastestvpn,
|
||||||
constants.Windscribe,
|
providers.Ivpn,
|
||||||
|
providers.Mullvad,
|
||||||
|
providers.Nordvpn,
|
||||||
|
providers.Protonvpn,
|
||||||
|
providers.Surfshark,
|
||||||
|
providers.Windscribe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !helpers.IsOneOf(*p.Name, validNames...) {
|
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
|
||||||
return fmt.Errorf("%w: %q can only be one of %s",
|
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||||
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.ServerSelection.validate(*p.Name, allServers)
|
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server selection settings validation failed: %w", err)
|
return fmt.Errorf("server selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.PortForwarding.validate(*p.Name)
|
err = p.PortForwarding.Validate(p.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("port forwarding settings validation failed: %w", err)
|
return fmt.Errorf("port forwarding: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -56,28 +63,22 @@ func (p *Provider) validate(vpnType string, allServers models.AllServers) (err e
|
|||||||
|
|
||||||
func (p *Provider) copy() (copied Provider) {
|
func (p *Provider) copy() (copied Provider) {
|
||||||
return Provider{
|
return Provider{
|
||||||
Name: helpers.CopyStringPtr(p.Name),
|
Name: p.Name,
|
||||||
ServerSelection: p.ServerSelection.copy(),
|
ServerSelection: p.ServerSelection.copy(),
|
||||||
PortForwarding: p.PortForwarding.copy(),
|
PortForwarding: p.PortForwarding.Copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) mergeWith(other Provider) {
|
|
||||||
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
|
|
||||||
p.ServerSelection.mergeWith(other.ServerSelection)
|
|
||||||
p.PortForwarding.mergeWith(other.PortForwarding)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) overrideWith(other Provider) {
|
func (p *Provider) overrideWith(other Provider) {
|
||||||
p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name)
|
p.Name = gosettings.OverrideWithComparable(p.Name, other.Name)
|
||||||
p.ServerSelection.overrideWith(other.ServerSelection)
|
p.ServerSelection.overrideWith(other.ServerSelection)
|
||||||
p.PortForwarding.overrideWith(other.PortForwarding)
|
p.PortForwarding.OverrideWith(other.PortForwarding)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) setDefaults() {
|
func (p *Provider) setDefaults() {
|
||||||
p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess)
|
p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess)
|
||||||
p.ServerSelection.setDefaults(*p.Name)
|
|
||||||
p.PortForwarding.setDefaults()
|
p.PortForwarding.setDefaults()
|
||||||
|
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Provider) String() string {
|
func (p Provider) String() string {
|
||||||
@@ -86,8 +87,42 @@ func (p Provider) String() string {
|
|||||||
|
|
||||||
func (p Provider) toLinesNode() (node *gotree.Node) {
|
func (p Provider) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("VPN provider settings:")
|
node = gotree.New("VPN provider settings:")
|
||||||
node.Appendf("Name: %s", *p.Name)
|
node.Appendf("Name: %s", p.Name)
|
||||||
node.AppendNode(p.ServerSelection.toLinesNode())
|
node.AppendNode(p.ServerSelection.toLinesNode())
|
||||||
node.AppendNode(p.PortForwarding.toLinesNode())
|
node.AppendNode(p.PortForwarding.toLinesNode())
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) read(r *reader.Reader, vpnType string) (err error) {
|
||||||
|
p.Name = readVPNServiceProvider(r, vpnType)
|
||||||
|
|
||||||
|
err = p.ServerSelection.read(r, p.Name, vpnType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.PortForwarding.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("port forwarding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readVPNServiceProvider(r *reader.Reader, vpnType string) (vpnProvider string) {
|
||||||
|
vpnProvider = r.String("VPN_SERVICE_PROVIDER", reader.RetroKeys("VPNSP"))
|
||||||
|
if vpnProvider == "" {
|
||||||
|
if vpnType != vpn.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
|
||||||
|
// retro compatibility
|
||||||
|
return providers.Custom
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
vpnProvider = strings.ToLower(vpnProvider)
|
||||||
|
if vpnProvider == "pia" { // retro compatibility
|
||||||
|
return providers.PrivateInternetAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
return vpnProvider
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,37 +3,65 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PublicIP contains settings for port forwarding.
|
// PublicIP contains settings for port forwarding.
|
||||||
type PublicIP struct {
|
type PublicIP struct {
|
||||||
// Period is the period to get the public IP address.
|
// Enabled is set to true to fetch the public ip address
|
||||||
// It can be set to 0 to disable periodic checking.
|
// information on VPN connection. It defaults to true.
|
||||||
// It cannot be nil for the internal state.
|
Enabled *bool
|
||||||
// TODO change to value and add enabled field
|
|
||||||
Period *time.Duration
|
|
||||||
// IPFilepath is the public IP address status file path
|
// IPFilepath is the public IP address status file path
|
||||||
// to use. It can be the empty string to indicate not
|
// to use. It can be the empty string to indicate not
|
||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
IPFilepath *string
|
IPFilepath *string
|
||||||
|
// APIs is the list of public ip APIs to use to fetch public IP information.
|
||||||
|
// If there is more than one API, the first one is used
|
||||||
|
// by default and the others are used as fallbacks in case of
|
||||||
|
// the service rate limiting us. It defaults to use all services,
|
||||||
|
// with the first one being ipinfo.io for historical reasons.
|
||||||
|
APIs []PublicIPAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicIPAPI struct {
|
||||||
|
// Name is the name of the public ip API service.
|
||||||
|
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
|
||||||
|
Name string
|
||||||
|
// Token is the token to use for the public ip API service.
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWith deep copies the receiving settings, overrides the copy with
|
||||||
|
// fields set in the partialUpdate argument, validates the new settings
|
||||||
|
// and returns them if they are valid, or returns an error otherwise.
|
||||||
|
// In all cases, the receiving settings are unmodified.
|
||||||
|
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
|
||||||
|
updatedSettings = p.copy()
|
||||||
|
updatedSettings.overrideWith(partialUpdate)
|
||||||
|
err = updatedSettings.validate()
|
||||||
|
if err != nil {
|
||||||
|
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
|
||||||
|
}
|
||||||
|
return updatedSettings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) validate() (err error) {
|
func (p PublicIP) validate() (err error) {
|
||||||
const minPeriod = 5 * time.Second
|
|
||||||
if *p.Period < minPeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be at least %s",
|
|
||||||
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *p.IPFilepath != "" { // optional
|
if *p.IPFilepath != "" { // optional
|
||||||
_, err := filepath.Abs(*p.IPFilepath)
|
_, err := filepath.Abs(*p.IPFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrPublicIPFilepathNotValid, err)
|
return fmt.Errorf("filepath is not valid: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, publicIPAPI := range p.APIs {
|
||||||
|
_, err = api.ParseProvider(publicIPAPI.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("API name: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,25 +70,27 @@ func (p PublicIP) validate() (err error) {
|
|||||||
|
|
||||||
func (p *PublicIP) copy() (copied PublicIP) {
|
func (p *PublicIP) copy() (copied PublicIP) {
|
||||||
return PublicIP{
|
return PublicIP{
|
||||||
Period: helpers.CopyDurationPtr(p.Period),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
|
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
||||||
|
APIs: gosettings.CopySlice(p.APIs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicIP) mergeWith(other PublicIP) {
|
|
||||||
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
|
|
||||||
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||||
p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
|
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
||||||
|
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicIP) setDefaults() {
|
func (p *PublicIP) setDefaults() {
|
||||||
const defaultPeriod = 12 * time.Hour
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
|
||||||
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
|
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
|
||||||
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
|
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
|
||||||
|
{Name: string(api.IPInfo)},
|
||||||
|
{Name: string(api.Cloudflare)},
|
||||||
|
{Name: string(api.IfConfigCo)},
|
||||||
|
{Name: string(api.IP2Location)},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) String() string {
|
func (p PublicIP) String() string {
|
||||||
@@ -68,22 +98,78 @@ func (p PublicIP) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||||
|
if !*p.Enabled {
|
||||||
|
return gotree.New("Public IP settings: disabled")
|
||||||
|
}
|
||||||
|
|
||||||
node = gotree.New("Public IP settings:")
|
node = gotree.New("Public IP settings:")
|
||||||
|
|
||||||
if *p.Period == 0 {
|
|
||||||
node.Appendf("Enabled: no")
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePeriod := "disabled"
|
|
||||||
if *p.Period > 0 {
|
|
||||||
updatePeriod = "every " + p.Period.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Fetching: %s", updatePeriod)
|
|
||||||
|
|
||||||
if *p.IPFilepath != "" {
|
if *p.IPFilepath != "" {
|
||||||
node.Appendf("IP file path: %s", *p.IPFilepath)
|
node.Appendf("IP file path: %s", *p.IPFilepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
|
||||||
|
if p.APIs[0].Token != "" {
|
||||||
|
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
|
||||||
|
}
|
||||||
|
node.Append(baseAPIString)
|
||||||
|
if len(p.APIs) > 1 {
|
||||||
|
backupAPIsNode := node.Append("Public IP data backup APIs:")
|
||||||
|
for i := 1; i < len(p.APIs); i++ {
|
||||||
|
message := p.APIs[i].Name
|
||||||
|
if p.APIs[i].Token != "" {
|
||||||
|
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
|
||||||
|
}
|
||||||
|
backupAPIsNode.Append(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
|
||||||
|
p.Enabled, err = readPublicIPEnabled(r, warner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.IPFilepath = r.Get("PUBLICIP_FILE",
|
||||||
|
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
|
||||||
|
|
||||||
|
apiNames := r.CSV("PUBLICIP_API")
|
||||||
|
if len(apiNames) > 0 {
|
||||||
|
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
|
||||||
|
p.APIs = make([]PublicIPAPI, len(apiNames))
|
||||||
|
for i := range apiNames {
|
||||||
|
p.APIs[i].Name = apiNames[i]
|
||||||
|
var token string
|
||||||
|
if i < len(apiTokens) { // only set token if it exists
|
||||||
|
token = apiTokens[i]
|
||||||
|
}
|
||||||
|
p.APIs[i].Token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
|
||||||
|
enabled *bool, err error,
|
||||||
|
) {
|
||||||
|
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if periodPtr == nil {
|
||||||
|
return r.BoolPtr("PUBLICIP_ENABLED")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *periodPtr == 0 {
|
||||||
|
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
|
||||||
|
return ptrTo(false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
|
||||||
|
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
|
||||||
|
"Please migrate to use PUBLICIP_ENABLED only in the future.")
|
||||||
|
return ptrTo(true), nil
|
||||||
|
}
|
||||||
|
|||||||
161
internal/configuration/settings/publicip_test.go
Normal file
161
internal/configuration/settings/publicip_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_PublicIP_read(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
makeReader func(ctrl *gomock.Controller) *reader.Reader
|
||||||
|
makeWarner func(ctrl *gomock.Controller) Warner
|
||||||
|
settings PublicIP
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"nothing_read": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single_api_no_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single_api_with_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_no_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo"},
|
||||||
|
{Name: "ip2location"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_with_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz,abc"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
{Name: "ip2location", Token: "abc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_with_and_without_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
{Name: "ip2location"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
reader := testCase.makeReader(ctrl)
|
||||||
|
var warner Warner
|
||||||
|
if testCase.makeWarner != nil {
|
||||||
|
warner = testCase.makeWarner(ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings PublicIP
|
||||||
|
err := settings.read(reader, warner)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.settings, settings)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,68 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ControlServer contains settings to customize the control server operation.
|
// ControlServer contains settings to customize the control server operation.
|
||||||
type ControlServer struct {
|
type ControlServer struct {
|
||||||
// Port is the listening port to use.
|
// Address is the listening address to use.
|
||||||
// It can be set to 0 to bind to a random port.
|
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
// TODO change to address
|
Address *string
|
||||||
Port *uint16
|
|
||||||
// Log can be true or false to enable logging on requests.
|
// Log can be true or false to enable logging on requests.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
Log *bool
|
Log *bool
|
||||||
|
// AuthFilePath is the path to the file containing the authentication
|
||||||
|
// configuration for the middleware.
|
||||||
|
// It cannot be empty in the internal state and defaults to
|
||||||
|
// /gluetun/auth/config.toml.
|
||||||
|
AuthFilePath string
|
||||||
|
// AuthDefaultRole is a JSON encoded object defining the default role
|
||||||
|
// that applies to all routes without a previously user-defined role assigned to.
|
||||||
|
AuthDefaultRole string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) validate() (err error) {
|
func (c ControlServer) validate() (err error) {
|
||||||
|
_, portStr, err := net.SplitHostPort(*c.Address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listening address is not valid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listening port it not valid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
uid := os.Getuid()
|
uid := os.Getuid()
|
||||||
const maxPrivilegedPort uint16 = 1023
|
const maxPrivilegedPort = 1023
|
||||||
if uid != 0 && *c.Port <= maxPrivilegedPort {
|
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
|
||||||
return fmt.Errorf("%w: %d when running with user ID %d",
|
return fmt.Errorf("%w: %d when running with user ID %d",
|
||||||
ErrControlServerPrivilegedPort, *c.Port, uid)
|
ErrControlServerPrivilegedPort, port, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
||||||
|
jsonDecoder.DisallowUnknownFields()
|
||||||
|
var role auth.Role
|
||||||
|
err = jsonDecoder.Decode(&role)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if role.Auth != "" {
|
||||||
|
err = role.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("default authentication role is not valid: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -33,30 +70,35 @@ func (c ControlServer) validate() (err error) {
|
|||||||
|
|
||||||
func (c *ControlServer) copy() (copied ControlServer) {
|
func (c *ControlServer) copy() (copied ControlServer) {
|
||||||
return ControlServer{
|
return ControlServer{
|
||||||
Port: helpers.CopyUint16Ptr(c.Port),
|
Address: gosettings.CopyPointer(c.Address),
|
||||||
Log: helpers.CopyBoolPtr(c.Log),
|
Log: gosettings.CopyPointer(c.Log),
|
||||||
|
AuthFilePath: c.AuthFilePath,
|
||||||
|
AuthDefaultRole: c.AuthDefaultRole,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeWith merges the other settings into any
|
|
||||||
// unset field of the receiver settings object.
|
|
||||||
func (c *ControlServer) mergeWith(other ControlServer) {
|
|
||||||
c.Port = helpers.MergeWithUint16(c.Port, other.Port)
|
|
||||||
c.Log = helpers.MergeWithBool(c.Log, other.Log)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (c *ControlServer) overrideWith(other ControlServer) {
|
func (c *ControlServer) overrideWith(other ControlServer) {
|
||||||
c.Port = helpers.MergeWithUint16(c.Port, other.Port)
|
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||||
c.Log = helpers.MergeWithBool(c.Log, other.Log)
|
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||||
|
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||||
|
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlServer) setDefaults() {
|
func (c *ControlServer) setDefaults() {
|
||||||
const defaultPort = 8000
|
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||||
c.Port = helpers.DefaultUint16(c.Port, defaultPort)
|
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||||
c.Log = helpers.DefaultBool(c.Log, true)
|
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||||
|
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
||||||
|
if c.AuthDefaultRole != "{}" {
|
||||||
|
var role auth.Role
|
||||||
|
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||||
|
role.Name = "default"
|
||||||
|
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
||||||
|
c.AuthDefaultRole = string(roleBytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) String() string {
|
func (c ControlServer) String() string {
|
||||||
@@ -65,7 +107,27 @@ func (c ControlServer) String() string {
|
|||||||
|
|
||||||
func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Control server settings:")
|
node = gotree.New("Control server settings:")
|
||||||
node.Appendf("Listening port: %d", *c.Port)
|
node.Appendf("Listening address: %s", *c.Address)
|
||||||
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
|
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||||
|
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||||
|
if c.AuthDefaultRole != "{}" {
|
||||||
|
var role auth.Role
|
||||||
|
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||||
|
node.AppendNode(role.ToLinesNode())
|
||||||
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ControlServer) read(r *reader.Reader) (err error) {
|
||||||
|
c.Log, err = r.BoolPtr("HTTP_CONTROL_SERVER_LOG")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||||
|
|
||||||
|
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||||
|
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,315 +1,362 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/configuration/settings/validation"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerSelection struct { //nolint:maligned
|
type ServerSelection struct {
|
||||||
// VPN is the VPN type which can be 'openvpn'
|
// VPN is the VPN type which can be 'openvpn'
|
||||||
// or 'wireguard'. It cannot be the empty string
|
// or 'wireguard'. It cannot be the empty string
|
||||||
// in the internal state.
|
// in the internal state.
|
||||||
VPN string
|
VPN string `json:"vpn"`
|
||||||
// TargetIP is the server endpoint IP address to use.
|
// TargetIP is the server endpoint IP address to use.
|
||||||
// It will override any IP address from the picked
|
// It will override any IP address from the picked
|
||||||
// built-in server. It cannot be nil in the internal
|
// built-in server. It cannot be the empty value in the internal
|
||||||
// state, and can be set to an empty net.IP{} to indicate
|
// state, and can be set to the unspecified address to indicate
|
||||||
// there is not target IP address to use.
|
// there is not target IP address to use.
|
||||||
TargetIP net.IP
|
TargetIP netip.Addr `json:"target_ip"`
|
||||||
// Counties is the list of countries to filter VPN servers with.
|
// Countries is the list of countries to filter VPN servers with.
|
||||||
Countries []string
|
Countries []string `json:"countries"`
|
||||||
|
// Categories is the list of categories to filter VPN servers with.
|
||||||
|
Categories []string `json:"categories"`
|
||||||
// Regions is the list of regions to filter VPN servers with.
|
// Regions is the list of regions to filter VPN servers with.
|
||||||
Regions []string
|
Regions []string `json:"regions"`
|
||||||
// Cities is the list of cities to filter VPN servers with.
|
// Cities is the list of cities to filter VPN servers with.
|
||||||
Cities []string
|
Cities []string `json:"cities"`
|
||||||
// ISPs is the list of ISP names to filter VPN servers with.
|
// ISPs is the list of ISP names to filter VPN servers with.
|
||||||
ISPs []string
|
ISPs []string `json:"isps"`
|
||||||
// Names is the list of server names to filter VPN servers with.
|
// Names is the list of server names to filter VPN servers with.
|
||||||
Names []string
|
Names []string `json:"names"`
|
||||||
// Numbers is the list of server numbers to filter VPN servers with.
|
// Numbers is the list of server numbers to filter VPN servers with.
|
||||||
Numbers []uint16
|
Numbers []uint16 `json:"numbers"`
|
||||||
// Hostnames is the list of hostnames to filter VPN servers with.
|
// Hostnames is the list of hostnames to filter VPN servers with.
|
||||||
Hostnames []string
|
Hostnames []string `json:"hostnames"`
|
||||||
// OwnedOnly is true if only VPN provider owned servers
|
// OwnedOnly is true if VPN provider servers that are not owned
|
||||||
// should be filtered. This is used with Mullvad.
|
// should be filtered. This is used with Mullvad.
|
||||||
OwnedOnly *bool
|
OwnedOnly *bool `json:"owned_only"`
|
||||||
// FreeOnly is true if only free VPN servers
|
// FreeOnly is true if VPN servers that are not free should
|
||||||
// should be filtered. This is used with ProtonVPN.
|
// be filtered. This is used with ProtonVPN and VPN Unlimited.
|
||||||
FreeOnly *bool
|
FreeOnly *bool `json:"free_only"`
|
||||||
// FreeOnly is true if only free VPN servers
|
// PremiumOnly is true if VPN servers that are not premium should
|
||||||
// should be filtered. This is used with ProtonVPN.
|
// be filtered. This is used with VPN Secure.
|
||||||
StreamOnly *bool
|
// TODO extend to providers using FreeOnly.
|
||||||
// MultiHopOnly is true if only multihop VPN servers
|
PremiumOnly *bool `json:"premium_only"`
|
||||||
|
// StreamOnly is true if VPN servers not for streaming should
|
||||||
|
// be filtered. This is used with ProtonVPN and VPNUnlimited.
|
||||||
|
StreamOnly *bool `json:"stream_only"`
|
||||||
|
// MultiHopOnly is true if VPN servers that are not multihop
|
||||||
// should be filtered. This is used with Surfshark.
|
// should be filtered. This is used with Surfshark.
|
||||||
MultiHopOnly *bool
|
MultiHopOnly *bool `json:"multi_hop_only"`
|
||||||
|
// PortForwardOnly is true if VPN servers that don't support
|
||||||
|
// port forwarding should be filtered. This is used with PIA
|
||||||
|
// and ProtonVPN.
|
||||||
|
PortForwardOnly *bool `json:"port_forward_only"`
|
||||||
|
// SecureCoreOnly is true if VPN servers without secure core should
|
||||||
|
// be filtered. This is used with ProtonVPN.
|
||||||
|
SecureCoreOnly *bool `json:"secure_core_only"`
|
||||||
|
// TorOnly is true if VPN servers without tor should
|
||||||
|
// be filtered. This is used with ProtonVPN.
|
||||||
|
TorOnly *bool `json:"tor_only"`
|
||||||
// OpenVPN contains settings to select OpenVPN servers
|
// OpenVPN contains settings to select OpenVPN servers
|
||||||
// and the final connection.
|
// and the final connection.
|
||||||
OpenVPN OpenVPNSelection
|
OpenVPN OpenVPNSelection `json:"openvpn"`
|
||||||
// Wireguard contains settings to select Wireguard servers
|
// Wireguard contains settings to select Wireguard servers
|
||||||
// and the final connection.
|
// and the final connection.
|
||||||
Wireguard WireguardSelection
|
Wireguard WireguardSelection `json:"wireguard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
|
||||||
|
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
|
||||||
|
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
|
||||||
|
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
|
||||||
|
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
||||||
|
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
|
||||||
|
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
|
||||||
|
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
|
||||||
|
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
|
||||||
|
)
|
||||||
|
|
||||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||||
allServers models.AllServers) (err error) {
|
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||||
|
) (err error) {
|
||||||
switch ss.VPN {
|
switch ss.VPN {
|
||||||
case constants.OpenVPN, constants.Wireguard:
|
case vpn.OpenVPN, vpn.Wireguard:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
var countryChoices, regionChoices, cityChoices,
|
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
|
||||||
ispChoices, nameChoices, hostnameChoices []string
|
|
||||||
switch vpnServiceProvider {
|
|
||||||
case constants.Custom:
|
|
||||||
case constants.Cyberghost:
|
|
||||||
servers := allServers.GetCyberghost()
|
|
||||||
countryChoices = constants.CyberghostCountryChoices(servers)
|
|
||||||
hostnameChoices = constants.CyberghostHostnameChoices(servers)
|
|
||||||
case constants.Expressvpn:
|
|
||||||
servers := allServers.GetExpressvpn()
|
|
||||||
countryChoices = constants.ExpressvpnCountriesChoices(servers)
|
|
||||||
cityChoices = constants.ExpressvpnCityChoices(servers)
|
|
||||||
hostnameChoices = constants.ExpressvpnHostnameChoices(servers)
|
|
||||||
case constants.Fastestvpn:
|
|
||||||
servers := allServers.GetFastestvpn()
|
|
||||||
countryChoices = constants.FastestvpnCountriesChoices(servers)
|
|
||||||
hostnameChoices = constants.FastestvpnHostnameChoices(servers)
|
|
||||||
case constants.HideMyAss:
|
|
||||||
servers := allServers.GetHideMyAss()
|
|
||||||
countryChoices = constants.HideMyAssCountryChoices(servers)
|
|
||||||
regionChoices = constants.HideMyAssRegionChoices(servers)
|
|
||||||
cityChoices = constants.HideMyAssCityChoices(servers)
|
|
||||||
hostnameChoices = constants.HideMyAssHostnameChoices(servers)
|
|
||||||
case constants.Ipvanish:
|
|
||||||
servers := allServers.GetIpvanish()
|
|
||||||
countryChoices = constants.IpvanishCountryChoices(servers)
|
|
||||||
cityChoices = constants.IpvanishCityChoices(servers)
|
|
||||||
hostnameChoices = constants.IpvanishHostnameChoices(servers)
|
|
||||||
case constants.Ivpn:
|
|
||||||
servers := allServers.GetIvpn()
|
|
||||||
countryChoices = constants.IvpnCountryChoices(servers)
|
|
||||||
cityChoices = constants.IvpnCityChoices(servers)
|
|
||||||
ispChoices = constants.IvpnISPChoices(servers)
|
|
||||||
hostnameChoices = constants.IvpnHostnameChoices(servers)
|
|
||||||
case constants.Mullvad:
|
|
||||||
servers := allServers.GetMullvad()
|
|
||||||
countryChoices = constants.MullvadCountryChoices(servers)
|
|
||||||
cityChoices = constants.MullvadCityChoices(servers)
|
|
||||||
ispChoices = constants.MullvadISPChoices(servers)
|
|
||||||
hostnameChoices = constants.MullvadHostnameChoices(servers)
|
|
||||||
case constants.Nordvpn:
|
|
||||||
servers := allServers.GetNordvpn()
|
|
||||||
regionChoices = constants.NordvpnRegionChoices(servers)
|
|
||||||
hostnameChoices = constants.NordvpnHostnameChoices(servers)
|
|
||||||
case constants.Perfectprivacy:
|
|
||||||
servers := allServers.GetPerfectprivacy()
|
|
||||||
cityChoices = constants.PerfectprivacyCityChoices(servers)
|
|
||||||
case constants.Privado:
|
|
||||||
servers := allServers.GetPrivado()
|
|
||||||
countryChoices = constants.PrivadoCountryChoices(servers)
|
|
||||||
regionChoices = constants.PrivadoRegionChoices(servers)
|
|
||||||
cityChoices = constants.PrivadoCityChoices(servers)
|
|
||||||
hostnameChoices = constants.PrivadoHostnameChoices(servers)
|
|
||||||
case constants.PrivateInternetAccess:
|
|
||||||
servers := allServers.GetPia()
|
|
||||||
regionChoices = constants.PIAGeoChoices(servers)
|
|
||||||
hostnameChoices = constants.PIAHostnameChoices(servers)
|
|
||||||
nameChoices = constants.PIANameChoices(servers)
|
|
||||||
case constants.Privatevpn:
|
|
||||||
servers := allServers.GetPrivatevpn()
|
|
||||||
countryChoices = constants.PrivatevpnCountryChoices(servers)
|
|
||||||
cityChoices = constants.PrivatevpnCityChoices(servers)
|
|
||||||
hostnameChoices = constants.PrivatevpnHostnameChoices(servers)
|
|
||||||
case constants.Protonvpn:
|
|
||||||
servers := allServers.GetProtonvpn()
|
|
||||||
countryChoices = constants.ProtonvpnCountryChoices(servers)
|
|
||||||
regionChoices = constants.ProtonvpnRegionChoices(servers)
|
|
||||||
cityChoices = constants.ProtonvpnCityChoices(servers)
|
|
||||||
nameChoices = constants.ProtonvpnNameChoices(servers)
|
|
||||||
hostnameChoices = constants.ProtonvpnHostnameChoices(servers)
|
|
||||||
case constants.Purevpn:
|
|
||||||
servers := allServers.GetPurevpn()
|
|
||||||
countryChoices = constants.PurevpnCountryChoices(servers)
|
|
||||||
regionChoices = constants.PurevpnRegionChoices(servers)
|
|
||||||
cityChoices = constants.PurevpnCityChoices(servers)
|
|
||||||
hostnameChoices = constants.PurevpnHostnameChoices(servers)
|
|
||||||
case constants.Surfshark:
|
|
||||||
servers := allServers.GetSurfshark()
|
|
||||||
countryChoices = constants.SurfsharkCountryChoices(servers)
|
|
||||||
cityChoices = constants.SurfsharkCityChoices(servers)
|
|
||||||
hostnameChoices = constants.SurfsharkHostnameChoices(servers)
|
|
||||||
regionChoices = constants.SurfsharkRegionChoices(servers)
|
|
||||||
// TODO v4 remove
|
|
||||||
regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...)
|
|
||||||
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
|
|
||||||
}
|
|
||||||
// Retro compatibility
|
|
||||||
// TODO remove in v4
|
|
||||||
*ss = surfsharkRetroRegion(*ss)
|
|
||||||
case constants.Torguard:
|
|
||||||
servers := allServers.GetTorguard()
|
|
||||||
countryChoices = constants.TorguardCountryChoices(servers)
|
|
||||||
cityChoices = constants.TorguardCityChoices(servers)
|
|
||||||
hostnameChoices = constants.TorguardHostnameChoices(servers)
|
|
||||||
case constants.VPNUnlimited:
|
|
||||||
servers := allServers.GetVPNUnlimited()
|
|
||||||
countryChoices = constants.VPNUnlimitedCountryChoices(servers)
|
|
||||||
cityChoices = constants.VPNUnlimitedCityChoices(servers)
|
|
||||||
hostnameChoices = constants.VPNUnlimitedHostnameChoices(servers)
|
|
||||||
case constants.Vyprvpn:
|
|
||||||
servers := allServers.GetVyprvpn()
|
|
||||||
regionChoices = constants.VyprvpnRegionChoices(servers)
|
|
||||||
case constants.Wevpn:
|
|
||||||
servers := allServers.GetWevpn()
|
|
||||||
cityChoices = constants.WevpnCityChoices(servers)
|
|
||||||
hostnameChoices = constants.WevpnHostnameChoices(servers)
|
|
||||||
case constants.Windscribe:
|
|
||||||
servers := allServers.GetWindscribe()
|
|
||||||
regionChoices = constants.WindscribeRegionChoices(servers)
|
|
||||||
cityChoices = constants.WindscribeCityChoices(servers)
|
|
||||||
hostnameChoices = constants.WindscribeHostnameChoices(servers)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
|
|
||||||
ispChoices, nameChoices, hostnameChoices)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // already wrapped error
|
return err // already wrapped error
|
||||||
}
|
}
|
||||||
|
|
||||||
if ss.VPN == constants.OpenVPN {
|
// Retro-compatibility
|
||||||
|
switch vpnServiceProvider {
|
||||||
|
case providers.Nordvpn:
|
||||||
|
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
|
||||||
|
case providers.Surfshark:
|
||||||
|
*ss = surfsharkRetroRegion(*ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateSubscriptionTierFilters(*ss, vpnServiceProvider)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateFeatureFilters(*ss, vpnServiceProvider)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ss.VPN == vpn.OpenVPN {
|
||||||
err = ss.OpenVPN.validate(vpnServiceProvider)
|
err = ss.OpenVPN.validate(vpnServiceProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("OpenVPN server selection settings validation failed: %w", err)
|
return fmt.Errorf("OpenVPN server selection settings: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = ss.Wireguard.validate(vpnServiceProvider)
|
err = ss.Wireguard.validate(vpnServiceProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Wireguard server selection settings validation failed: %w", err)
|
return fmt.Errorf("Wireguard server selection settings: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLocationFilterChoices(vpnServiceProvider string,
|
||||||
|
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
|
||||||
|
filterChoices models.FilterChoices, err error,
|
||||||
|
) {
|
||||||
|
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
|
||||||
|
|
||||||
|
if vpnServiceProvider == providers.Surfshark {
|
||||||
|
// // Retro compatibility
|
||||||
|
// TODO v4 remove
|
||||||
|
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
|
||||||
|
err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
|
||||||
|
if err != nil {
|
||||||
|
// Only return error comparing with newer regions, we don't want to confuse the user
|
||||||
|
// with the retro regions in the error message.
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
|
||||||
|
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterChoices, nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateServerFilters validates filters against the choices given as arguments.
|
// validateServerFilters validates filters against the choices given as arguments.
|
||||||
// Set an argument to nil to pass the check for a particular filter.
|
// Set an argument to nil to pass the check for a particular filter.
|
||||||
func validateServerFilters(settings ServerSelection,
|
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
||||||
countryChoices, regionChoices, cityChoices, ispChoices,
|
vpnServiceProvider string, warner Warner,
|
||||||
nameChoices, hostnameChoices []string) (err error) {
|
) (err error) {
|
||||||
if countryChoices != nil {
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
|
||||||
if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
|
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if regionChoices != nil {
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
|
||||||
if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
|
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cityChoices != nil {
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
|
||||||
if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
|
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ispChoices != nil {
|
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
|
||||||
if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
|
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hostnameChoices != nil {
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
|
||||||
if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if nameChoices != nil {
|
if vpnServiceProvider == providers.Custom {
|
||||||
if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil {
|
switch len(settings.Names) {
|
||||||
return fmt.Errorf("%w: %s", ErrNameNotValid, err)
|
case 0:
|
||||||
|
case 1:
|
||||||
|
// Allow a single name to be specified for the custom provider in case
|
||||||
|
// the user wants to use VPN server side port forwarding with PIA
|
||||||
|
// which requires a server name for TLS verification.
|
||||||
|
filterChoices.Names = settings.Names
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %d names specified instead of "+
|
||||||
|
"0 or 1 for the custom provider",
|
||||||
|
ErrNameNotValid, len(settings.Names))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
|
||||||
return ServerSelection{
|
warner Warner,
|
||||||
VPN: ss.VPN,
|
) (err error) {
|
||||||
TargetIP: helpers.CopyIP(ss.TargetIP),
|
if len(values) > 0 && len(choices) == 0 {
|
||||||
Countries: helpers.CopyStringSlice(ss.Countries),
|
return fmt.Errorf("%w", validate.ErrNoChoice)
|
||||||
Regions: helpers.CopyStringSlice(ss.Regions),
|
}
|
||||||
Cities: helpers.CopyStringSlice(ss.Cities),
|
|
||||||
ISPs: helpers.CopyStringSlice(ss.ISPs),
|
set := make(map[string]struct{}, len(choices))
|
||||||
Hostnames: helpers.CopyStringSlice(ss.Hostnames),
|
for _, choice := range choices {
|
||||||
Names: helpers.CopyStringSlice(ss.Names),
|
lowercaseChoice := strings.ToLower(choice)
|
||||||
Numbers: helpers.CopyUint16Slice(ss.Numbers),
|
set[lowercaseChoice] = struct{}{}
|
||||||
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
|
}
|
||||||
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
|
|
||||||
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
|
invalidValues := make([]string, 0, len(values))
|
||||||
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
|
for _, value := range values {
|
||||||
OpenVPN: ss.OpenVPN.copy(),
|
lowercaseValue := strings.ToLower(value)
|
||||||
Wireguard: ss.Wireguard.copy(),
|
_, ok := set[lowercaseValue]
|
||||||
|
if ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
invalidValues = append(invalidValues, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(invalidValues) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case len(values):
|
||||||
|
return fmt.Errorf("%w: none of %s is one of the choices available %s",
|
||||||
|
validate.ErrValueNotOneOf, strings.Join(values, ", "), strings.Join(choices, ", "))
|
||||||
|
default:
|
||||||
|
warner.Warn(fmt.Sprintf("values %s are not in choices %s",
|
||||||
|
strings.Join(invalidValues, ", "), strings.Join(choices, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvider string) error {
|
||||||
|
switch {
|
||||||
|
case *settings.FreeOnly &&
|
||||||
|
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
||||||
|
return fmt.Errorf("%w", ErrFreeOnlyNotSupported)
|
||||||
|
case *settings.PremiumOnly &&
|
||||||
|
!helpers.IsOneOf(vpnServiceProvider, providers.VPNSecure):
|
||||||
|
return fmt.Errorf("%w", ErrPremiumOnlyNotSupported)
|
||||||
|
case *settings.FreeOnly && *settings.PremiumOnly:
|
||||||
|
return fmt.Errorf("%w", ErrFreePremiumBothSet)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ServerSelection) mergeWith(other ServerSelection) {
|
func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
|
||||||
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
|
switch {
|
||||||
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
|
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
|
||||||
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
|
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
|
||||||
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
|
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
|
||||||
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
|
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
|
||||||
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
|
case *settings.StreamOnly &&
|
||||||
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
|
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
||||||
ss.Names = helpers.MergeStringSlices(ss.Names, other.Names)
|
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
|
||||||
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
|
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
|
||||||
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
|
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
|
||||||
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
|
case *settings.PortForwardOnly &&
|
||||||
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
|
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
|
||||||
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
|
||||||
|
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
|
||||||
|
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
|
||||||
|
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
|
||||||
|
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ss.OpenVPN.mergeWith(other.OpenVPN)
|
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||||
ss.Wireguard.mergeWith(other.Wireguard)
|
return ServerSelection{
|
||||||
|
VPN: ss.VPN,
|
||||||
|
TargetIP: ss.TargetIP,
|
||||||
|
Countries: gosettings.CopySlice(ss.Countries),
|
||||||
|
Categories: gosettings.CopySlice(ss.Categories),
|
||||||
|
Regions: gosettings.CopySlice(ss.Regions),
|
||||||
|
Cities: gosettings.CopySlice(ss.Cities),
|
||||||
|
ISPs: gosettings.CopySlice(ss.ISPs),
|
||||||
|
Hostnames: gosettings.CopySlice(ss.Hostnames),
|
||||||
|
Names: gosettings.CopySlice(ss.Names),
|
||||||
|
Numbers: gosettings.CopySlice(ss.Numbers),
|
||||||
|
OwnedOnly: gosettings.CopyPointer(ss.OwnedOnly),
|
||||||
|
FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
|
||||||
|
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
|
||||||
|
StreamOnly: gosettings.CopyPointer(ss.StreamOnly),
|
||||||
|
SecureCoreOnly: gosettings.CopyPointer(ss.SecureCoreOnly),
|
||||||
|
TorOnly: gosettings.CopyPointer(ss.TorOnly),
|
||||||
|
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
|
||||||
|
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
|
||||||
|
OpenVPN: ss.OpenVPN.copy(),
|
||||||
|
Wireguard: ss.Wireguard.copy(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||||
ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
|
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||||
ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
|
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
||||||
ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
|
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||||
ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
|
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||||
ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
|
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||||
ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
|
ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities)
|
||||||
ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
|
ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs)
|
||||||
ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names)
|
ss.Hostnames = gosettings.OverrideWithSlice(ss.Hostnames, other.Hostnames)
|
||||||
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
|
ss.Names = gosettings.OverrideWithSlice(ss.Names, other.Names)
|
||||||
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
|
ss.Numbers = gosettings.OverrideWithSlice(ss.Numbers, other.Numbers)
|
||||||
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
|
ss.OwnedOnly = gosettings.OverrideWithPointer(ss.OwnedOnly, other.OwnedOnly)
|
||||||
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
|
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
|
||||||
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
|
||||||
|
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly)
|
||||||
|
ss.SecureCoreOnly = gosettings.OverrideWithPointer(ss.SecureCoreOnly, other.SecureCoreOnly)
|
||||||
|
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
|
||||||
|
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
|
||||||
|
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
|
||||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||||
ss.Wireguard.overrideWith(other.Wireguard)
|
ss.Wireguard.overrideWith(other.Wireguard)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ServerSelection) setDefaults(vpnProvider string) {
|
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||||
ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN)
|
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||||
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
|
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
||||||
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
|
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||||
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
|
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||||
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
|
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||||
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
|
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false)
|
||||||
|
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||||
|
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||||
|
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||||
|
defaultPortForwardOnly := portForwardingEnabled &&
|
||||||
|
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
||||||
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
ss.Wireguard.setDefaults()
|
ss.Wireguard.setDefaults()
|
||||||
}
|
}
|
||||||
@@ -321,7 +368,7 @@ func (ss ServerSelection) String() string {
|
|||||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Server selection settings:")
|
node = gotree.New("Server selection settings:")
|
||||||
node.Appendf("VPN type: %s", ss.VPN)
|
node.Appendf("VPN type: %s", ss.VPN)
|
||||||
if len(ss.TargetIP) > 0 {
|
if !ss.TargetIP.IsUnspecified() {
|
||||||
node.Appendf("Target IP address: %s", ss.TargetIP)
|
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +376,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ss.Categories) > 0 {
|
||||||
|
node.Appendf("Categories: %s", strings.Join(ss.Categories, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
if len(ss.Regions) > 0 {
|
if len(ss.Regions) > 0 {
|
||||||
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
|
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
|
||||||
}
|
}
|
||||||
@@ -364,15 +415,31 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Free only servers: yes")
|
node.Appendf("Free only servers: yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *ss.PremiumOnly {
|
||||||
|
node.Appendf("Premium only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
if *ss.StreamOnly {
|
if *ss.StreamOnly {
|
||||||
node.Appendf("Stream only servers: yes")
|
node.Appendf("Stream only servers: yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *ss.SecureCoreOnly {
|
||||||
|
node.Appendf("Secure Core only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.TorOnly {
|
||||||
|
node.Appendf("Tor only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
if *ss.MultiHopOnly {
|
if *ss.MultiHopOnly {
|
||||||
node.Appendf("Multi-hop only servers: yes")
|
node.Appendf("Multi-hop only servers: yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ss.VPN == constants.OpenVPN {
|
if *ss.PortForwardOnly {
|
||||||
|
node.Appendf("Port forwarding only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ss.VPN == vpn.OpenVPN {
|
||||||
node.AppendNode(ss.OpenVPN.toLinesNode())
|
node.AppendNode(ss.OpenVPN.toLinesNode())
|
||||||
} else {
|
} else {
|
||||||
node.AppendNode(ss.Wireguard.toLinesNode())
|
node.AppendNode(ss.Wireguard.toLinesNode())
|
||||||
@@ -384,6 +451,96 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
// WithDefaults is a shorthand using setDefaults.
|
// WithDefaults is a shorthand using setDefaults.
|
||||||
// It's used in unit tests in other packages.
|
// It's used in unit tests in other packages.
|
||||||
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
||||||
ss.setDefaults(provider)
|
const portForwardingEnabled = false
|
||||||
|
ss.setDefaults(provider, portForwardingEnabled)
|
||||||
return ss
|
return ss
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss *ServerSelection) read(r *reader.Reader,
|
||||||
|
vpnProvider, vpnType string,
|
||||||
|
) (err error) {
|
||||||
|
ss.VPN = vpnType
|
||||||
|
|
||||||
|
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||||
|
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
countriesRetroKeys := []string{"COUNTRY"}
|
||||||
|
if vpnProvider == providers.Cyberghost {
|
||||||
|
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
||||||
|
}
|
||||||
|
ss.Countries = r.CSV("SERVER_COUNTRIES", reader.RetroKeys(countriesRetroKeys...))
|
||||||
|
|
||||||
|
ss.Regions = r.CSV("SERVER_REGIONS", reader.RetroKeys("REGION"))
|
||||||
|
ss.Cities = r.CSV("SERVER_CITIES", reader.RetroKeys("CITY"))
|
||||||
|
ss.ISPs = r.CSV("ISP")
|
||||||
|
ss.Hostnames = r.CSV("SERVER_HOSTNAMES", reader.RetroKeys("SERVER_HOSTNAME"))
|
||||||
|
ss.Names = r.CSV("SERVER_NAMES", reader.RetroKeys("SERVER_NAME"))
|
||||||
|
ss.Numbers, err = r.CSVUint16("SERVER_NUMBER")
|
||||||
|
ss.Categories = r.CSV("SERVER_CATEGORIES")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mullvad only
|
||||||
|
ss.OwnedOnly, err = r.BoolPtr("OWNED_ONLY", reader.RetroKeys("OWNED"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPNUnlimited and ProtonVPN only
|
||||||
|
ss.FreeOnly, err = r.BoolPtr("FREE_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPNSecure only
|
||||||
|
ss.PremiumOnly, err = r.BoolPtr("PREMIUM_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surfshark only
|
||||||
|
ss.MultiHopOnly, err = r.BoolPtr("MULTIHOP_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPNUnlimited and ProtonVPN only
|
||||||
|
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtonVPN only
|
||||||
|
ss.SecureCoreOnly, err = r.BoolPtr("SECURE_CORE_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtonVPN only
|
||||||
|
ss.TorOnly, err = r.BoolPtr("TOR_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PIA and ProtonVPN only
|
||||||
|
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ss.OpenVPN.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ss.Wireguard.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/pprof"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,16 +22,24 @@ type Settings struct {
|
|||||||
Log Log
|
Log Log
|
||||||
PublicIP PublicIP
|
PublicIP PublicIP
|
||||||
Shadowsocks Shadowsocks
|
Shadowsocks Shadowsocks
|
||||||
|
Storage Storage
|
||||||
System System
|
System System
|
||||||
Updater Updater
|
Updater Updater
|
||||||
Version Version
|
Version Version
|
||||||
VPN VPN
|
VPN VPN
|
||||||
|
Pprof pprof.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterChoicesGetter interface {
|
||||||
|
GetFilterChoices(provider string) models.FilterChoices
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates all the settings and returns an error
|
// Validate validates all the settings and returns an error
|
||||||
// if one of them is not valid.
|
// if one of them is not valid.
|
||||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
func (s *Settings) Validate(allServers models.AllServers) (err error) {
|
func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool,
|
||||||
|
warner Warner,
|
||||||
|
) (err error) {
|
||||||
nameToValidation := map[string]func() error{
|
nameToValidation := map[string]func() error{
|
||||||
"control server": s.ControlServer.validate,
|
"control server": s.ControlServer.validate,
|
||||||
"dns": s.DNS.validate,
|
"dns": s.DNS.validate,
|
||||||
@@ -35,18 +49,20 @@ func (s *Settings) Validate(allServers models.AllServers) (err error) {
|
|||||||
"log": s.Log.validate,
|
"log": s.Log.validate,
|
||||||
"public ip check": s.PublicIP.validate,
|
"public ip check": s.PublicIP.validate,
|
||||||
"shadowsocks": s.Shadowsocks.validate,
|
"shadowsocks": s.Shadowsocks.validate,
|
||||||
|
"storage": s.Storage.validate,
|
||||||
"system": s.System.validate,
|
"system": s.System.validate,
|
||||||
"updater": s.Updater.Validate,
|
"updater": s.Updater.Validate,
|
||||||
"version": s.Version.validate,
|
"version": s.Version.validate,
|
||||||
|
// Pprof validation done in pprof constructor
|
||||||
"VPN": func() error {
|
"VPN": func() error {
|
||||||
return s.VPN.validate(allServers)
|
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, validation := range nameToValidation {
|
for name, validation := range nameToValidation {
|
||||||
err = validation()
|
err = validation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed validating %s settings: %w", name, err)
|
return fmt.Errorf("%s settings: %w", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,30 +79,18 @@ func (s *Settings) copy() (copied Settings) {
|
|||||||
Log: s.Log.copy(),
|
Log: s.Log.copy(),
|
||||||
PublicIP: s.PublicIP.copy(),
|
PublicIP: s.PublicIP.copy(),
|
||||||
Shadowsocks: s.Shadowsocks.copy(),
|
Shadowsocks: s.Shadowsocks.copy(),
|
||||||
|
Storage: s.Storage.copy(),
|
||||||
System: s.System.copy(),
|
System: s.System.copy(),
|
||||||
Updater: s.Updater.copy(),
|
Updater: s.Updater.copy(),
|
||||||
Version: s.Version.copy(),
|
Version: s.Version.copy(),
|
||||||
VPN: s.VPN.copy(),
|
VPN: s.VPN.Copy(),
|
||||||
|
Pprof: s.Pprof.Copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Settings) MergeWith(other Settings) {
|
|
||||||
s.ControlServer.mergeWith(other.ControlServer)
|
|
||||||
s.DNS.mergeWith(other.DNS)
|
|
||||||
s.Firewall.mergeWith(other.Firewall)
|
|
||||||
s.Health.MergeWith(other.Health)
|
|
||||||
s.HTTPProxy.mergeWith(other.HTTPProxy)
|
|
||||||
s.Log.mergeWith(other.Log)
|
|
||||||
s.PublicIP.mergeWith(other.PublicIP)
|
|
||||||
s.Shadowsocks.mergeWith(other.Shadowsocks)
|
|
||||||
s.System.mergeWith(other.System)
|
|
||||||
s.Updater.mergeWith(other.Updater)
|
|
||||||
s.Version.mergeWith(other.Version)
|
|
||||||
s.VPN.mergeWith(other.VPN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) OverrideWith(other Settings,
|
func (s *Settings) OverrideWith(other Settings,
|
||||||
allServers models.AllServers) (err error) {
|
filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner,
|
||||||
|
) (err error) {
|
||||||
patchedSettings := s.copy()
|
patchedSettings := s.copy()
|
||||||
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
||||||
patchedSettings.DNS.overrideWith(other.DNS)
|
patchedSettings.DNS.overrideWith(other.DNS)
|
||||||
@@ -96,11 +100,13 @@ func (s *Settings) OverrideWith(other Settings,
|
|||||||
patchedSettings.Log.overrideWith(other.Log)
|
patchedSettings.Log.overrideWith(other.Log)
|
||||||
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
||||||
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
||||||
|
patchedSettings.Storage.overrideWith(other.Storage)
|
||||||
patchedSettings.System.overrideWith(other.System)
|
patchedSettings.System.overrideWith(other.System)
|
||||||
patchedSettings.Updater.overrideWith(other.Updater)
|
patchedSettings.Updater.overrideWith(other.Updater)
|
||||||
patchedSettings.Version.overrideWith(other.Version)
|
patchedSettings.Version.overrideWith(other.Version)
|
||||||
patchedSettings.VPN.overrideWith(other.VPN)
|
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||||
err = patchedSettings.Validate(allServers)
|
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||||
|
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -117,10 +123,12 @@ func (s *Settings) SetDefaults() {
|
|||||||
s.Log.setDefaults()
|
s.Log.setDefaults()
|
||||||
s.PublicIP.setDefaults()
|
s.PublicIP.setDefaults()
|
||||||
s.Shadowsocks.setDefaults()
|
s.Shadowsocks.setDefaults()
|
||||||
|
s.Storage.setDefaults()
|
||||||
s.System.setDefaults()
|
s.System.setDefaults()
|
||||||
s.Updater.SetDefaults()
|
|
||||||
s.Version.setDefaults()
|
s.Version.setDefaults()
|
||||||
s.VPN.setDefaults()
|
s.VPN.setDefaults()
|
||||||
|
s.Updater.SetDefaults(s.VPN.Provider.Name)
|
||||||
|
s.Pprof.SetDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Settings) String() string {
|
func (s Settings) String() string {
|
||||||
@@ -138,10 +146,77 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
|||||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||||
node.AppendNode(s.ControlServer.toLinesNode())
|
node.AppendNode(s.ControlServer.toLinesNode())
|
||||||
|
node.AppendNode(s.Storage.toLinesNode())
|
||||||
node.AppendNode(s.System.toLinesNode())
|
node.AppendNode(s.System.toLinesNode())
|
||||||
node.AppendNode(s.PublicIP.toLinesNode())
|
node.AppendNode(s.PublicIP.toLinesNode())
|
||||||
node.AppendNode(s.Updater.toLinesNode())
|
node.AppendNode(s.Updater.toLinesNode())
|
||||||
node.AppendNode(s.Version.toLinesNode())
|
node.AppendNode(s.Version.toLinesNode())
|
||||||
|
node.AppendNode(s.Pprof.ToLinesNode())
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Settings) Warnings() (warnings []string) {
|
||||||
|
if s.VPN.Provider.Name == providers.HideMyAss {
|
||||||
|
warnings = append(warnings, "HideMyAss dropped support for Linux OpenVPN "+
|
||||||
|
" so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if helpers.IsOneOf(s.VPN.Provider.Name, providers.SlickVPN) &&
|
||||||
|
s.VPN.Type == vpn.OpenVPN {
|
||||||
|
warnings = append(warnings, "OpenVPN 2.5 and 2.6 use OpenSSL 3 "+
|
||||||
|
"which prohibits the usage of weak security in today's standards. "+
|
||||||
|
s.VPN.Provider.Name+" uses weak security which is out "+
|
||||||
|
"of Gluetun's control so the only workaround is to allow such weaknesses "+
|
||||||
|
`using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+
|
||||||
|
"You might want to reach to your provider so they upgrade their certificates. "+
|
||||||
|
"Once this is done, you will have to let the Gluetun maintainers know "+
|
||||||
|
"by creating an issue, attaching the new certificate and we will update Gluetun.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove in v4
|
||||||
|
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||||
|
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||||
|
" so the local forwarding DNS server will not be used."+
|
||||||
|
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
|
||||||
|
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||||
|
" corresponding to the first DNS provider chosen is used.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||||
|
warnings := readObsolete(r)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
warner.Warn(warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
readFunctions := map[string]func(r *reader.Reader) error{
|
||||||
|
"control server": s.ControlServer.read,
|
||||||
|
"DNS": s.DNS.read,
|
||||||
|
"firewall": s.Firewall.read,
|
||||||
|
"health": s.Health.Read,
|
||||||
|
"http proxy": s.HTTPProxy.read,
|
||||||
|
"log": s.Log.read,
|
||||||
|
"public ip": func(r *reader.Reader) error {
|
||||||
|
return s.PublicIP.read(r, warner)
|
||||||
|
},
|
||||||
|
"shadowsocks": s.Shadowsocks.read,
|
||||||
|
"storage": s.Storage.read,
|
||||||
|
"system": s.System.read,
|
||||||
|
"updater": s.Updater.read,
|
||||||
|
"version": s.Version.read,
|
||||||
|
"VPN": s.VPN.read,
|
||||||
|
"profiling": s.Pprof.Read,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, read := range readFunctions {
|
||||||
|
err = read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading %s settings: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user