Compare commits
1514 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c930a4e1be | ||
|
|
22834e9477 | ||
|
|
62c2679da2 | ||
|
|
5e9ae9fa1f | ||
|
|
0f19bcfebd | ||
|
|
83fc91d3c6 | ||
|
|
4adeec8223 | ||
|
|
64bfbaa45d | ||
|
|
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 | ||
|
|
55e609cbf4 | ||
|
|
f7319eb7a5 | ||
|
|
2cff64fd80 | ||
|
|
fdc0db07e0 | ||
|
|
779cb18590 | ||
|
|
a1a1128d6d | ||
|
|
7a50daca7c | ||
|
|
2ad23a09e8 | ||
|
|
7e2ea4d74d | ||
|
|
4bdce76041 | ||
|
|
6f5a78c22b | ||
|
|
d6d529278e | ||
|
|
a430d15ac5 | ||
|
|
b0c2d5f299 | ||
|
|
b32c01c11a | ||
|
|
05f42f0cb8 | ||
|
|
3efe3a524a | ||
|
|
8a788dfca5 | ||
|
|
d5f1589ea1 | ||
|
|
c40c8413b5 | ||
|
|
ae074dfb2b | ||
|
|
2c8a8f6cd5 | ||
|
|
4c7a09c228 | ||
|
|
ab39edc692 | ||
|
|
6132cd9df2 | ||
|
|
7d824a5179 | ||
|
|
46738b2934 | ||
|
|
ca82fcb48e | ||
|
|
cfa3bb3b64 | ||
|
|
b9a9319cb4 | ||
|
|
77e4317135 | ||
|
|
b10d97e53a | ||
|
|
648a4c04d7 | ||
|
|
3ca674dca7 | ||
|
|
fa97fd496e | ||
|
|
c76a7ee8da | ||
|
|
80f6b78332 | ||
|
|
8dc54a7c44 | ||
|
|
8f080c537b | ||
|
|
427cf86f44 | ||
|
|
2d244c08e7 | ||
|
|
82c0f523aa | ||
|
|
c07a0b0ada | ||
|
|
e4c306c0ee | ||
|
|
6ffb94f819 | ||
|
|
142238e8b7 | ||
|
|
678e23c7d6 | ||
|
|
0abcebe1d8 | ||
|
|
f398af1169 | ||
|
|
afbea415e3 | ||
|
|
225bd5d25b | ||
|
|
3651cc6161 | ||
|
|
dc674014ff | ||
|
|
0e0e03949d | ||
|
|
f5bf5c236a | ||
|
|
94480ecabb | ||
|
|
31ef9b1d45 | ||
|
|
bf76132fd4 | ||
|
|
8cc2983318 | ||
|
|
caeca18ed7 | ||
|
|
50febb41ff | ||
|
|
79293e067c | ||
|
|
f45be80591 | ||
|
|
d405ba8dca | ||
|
|
ca975b1c01 | ||
|
|
e0e3ca3832 | ||
|
|
e7c952cbf7 | ||
|
|
85ad2dd39a | ||
|
|
0c4f0ec17b | ||
|
|
5ad4136955 | ||
|
|
a432de95a9 | ||
|
|
1d25a0e18c | ||
|
|
29fd95685f | ||
|
|
62a6016882 | ||
|
|
18a4a79763 | ||
|
|
56ea722f93 | ||
|
|
d2ab974933 | ||
|
|
37d7a8b5fe | ||
|
|
e4dcadd825 | ||
|
|
fee99e9fe3 | ||
|
|
8ac4826126 | ||
|
|
7deb12e06d | ||
|
|
d6e218141b | ||
|
|
f44121b044 | ||
|
|
5d8d92462d | ||
|
|
985cf7b7dd | ||
|
|
dcbc10fd57 | ||
|
|
79f243e98d | ||
|
|
cf95692b93 | ||
|
|
d8e008606f | ||
|
|
3cd26a9f61 | ||
|
|
5d74320ee7 | ||
|
|
f9aadeef1c | ||
|
|
625de1c834 | ||
|
|
1c0a3ed1a4 | ||
|
|
03ba9169f4 | ||
|
|
c22e0e9db7 | ||
|
|
6bcbaf085d | ||
|
|
9a1d9c5d74 | ||
|
|
59a3a072e0 | ||
|
|
9f001bbc06 | ||
|
|
b8356b60a6 | ||
|
|
e2e218c74b | ||
|
|
3bf23cbae5 | ||
|
|
da562d8206 | ||
|
|
81bf83db13 | ||
|
|
7a25dcd130 | ||
|
|
877c7e1a9f | ||
|
|
77b2512745 | ||
|
|
749b73ef15 | ||
|
|
e499eca12c | ||
|
|
80f25c34e5 | ||
|
|
61677fbce2 | ||
|
|
dc6171185e | ||
|
|
f7e4331e93 | ||
|
|
1340511b64 | ||
|
|
c3078f84e8 | ||
|
|
9f65157a0d | ||
|
|
89166cdabf | ||
|
|
b872973e8b | ||
|
|
2000e72357 | ||
|
|
836e53642d | ||
|
|
af3f882bb8 | ||
|
|
2ab05b9350 | ||
|
|
1022eb8a6e | ||
|
|
15fe62de32 | ||
|
|
83d87f83f9 | ||
|
|
76a0c1f6c4 | ||
|
|
a1588302a7 | ||
|
|
91ce790b6b | ||
|
|
5d3982c2d2 | ||
|
|
2cf7f7b268 | ||
|
|
8645d978ba | ||
|
|
cc18b158f4 | ||
|
|
0730b6db6e | ||
|
|
3d2a360401 | ||
|
|
0c60dab384 | ||
|
|
f5f0ad7f28 | ||
|
|
f807f756eb | ||
|
|
11af6c10f1 | ||
|
|
40342619e7 | ||
|
|
19bf62c21f | ||
|
|
2ea00d149f | ||
|
|
cc677bde93 | ||
|
|
6627cda96c | ||
|
|
cade2732b0 | ||
|
|
541a4a3271 | ||
|
|
0eccd068e5 | ||
|
|
87f4b9e422 | ||
|
|
bcaf2e42fd | ||
|
|
d39201f9b3 | ||
|
|
8ac2a816c3 | ||
|
|
344f1bf9ee | ||
|
|
f0a006fc43 | ||
|
|
145da0b21d | ||
|
|
094de89a3e | ||
|
|
65ace12def | ||
|
|
9afe455635 | ||
|
|
45ce422a89 | ||
|
|
4a0738cd49 | ||
|
|
6b6caa435f | ||
|
|
f9cb71027c | ||
|
|
82ac568ee3 | ||
|
|
61afdce788 | ||
|
|
119cac5a67 | ||
|
|
c6fedd9214 | ||
|
|
da525e039d | ||
|
|
29d92fd307 | ||
|
|
3863cc439e | ||
|
|
b1cfc03fc5 | ||
|
|
f706071048 | ||
|
|
501ae2741b | ||
|
|
5b75635386 | ||
|
|
2901db3cf3 | ||
|
|
6c2a3e36b5 | ||
|
|
8b125e6e95 | ||
|
|
e1cc14e055 | ||
|
|
d6659552df | ||
|
|
67001fa958 | ||
|
|
ffeeae91ab | ||
|
|
04fad1b781 | ||
|
|
dcaf952986 | ||
|
|
ca3b9e892d | ||
|
|
9f12ffc069 | ||
|
|
0d6800a515 | ||
|
|
b3d8b78205 | ||
|
|
ee82a85543 | ||
|
|
7907146aaf | ||
|
|
1a677ce4f7 | ||
|
|
f1a6594474 | ||
|
|
f1a82d9d9c | ||
|
|
8b52af0d03 | ||
|
|
dbf5c569ea | ||
|
|
06a2d79cb4 | ||
|
|
eb6238ee52 | ||
|
|
f41fec57ed | ||
|
|
c348343b22 | ||
|
|
b69dcb62e3 | ||
|
|
e4a260f148 | ||
|
|
614eb10d67 | ||
|
|
0bfd58a3f5 | ||
|
|
ff56857fc8 | ||
|
|
8d258feff7 | ||
|
|
96ee1bbfb2 | ||
|
|
abaf688ad8 | ||
|
|
bec8ff27ae | ||
|
|
7191d4e911 | ||
|
|
6f59bc3037 | ||
|
|
5c2286f4e8 | ||
|
|
9218c7ef19 | ||
|
|
3d8e61900b | ||
|
|
105d81c018 | ||
|
|
d4ca5cf257 | ||
|
|
05018ec971 | ||
|
|
538bc72c3c | ||
|
|
0027a76c49 | ||
|
|
a0cb6fabfd | ||
|
|
9e5400f52d | ||
|
|
7a1d0ff3ec | ||
|
|
d9fbecaa01 | ||
|
|
ecdf9396a5 | ||
|
|
df51aa40f4 | ||
|
|
996942af47 | ||
|
|
f17a4eae3e | ||
|
|
c515603d2f | ||
|
|
14c3b6429b | ||
|
|
bd110b960b | ||
|
|
3ad4319163 | ||
|
|
97340ec70b | ||
|
|
5140a7b010 | ||
|
|
bd74879303 | ||
|
|
da30ae287f | ||
|
|
6a545aa088 | ||
|
|
384a4bae3a | ||
|
|
e65f924cd7 | ||
|
|
9105b33e9f | ||
|
|
cc2235653a | ||
|
|
a00de75f61 | ||
|
|
836412b032 | ||
|
|
ba16270059 | ||
|
|
2c73672e64 | ||
|
|
74b7c81195 | ||
|
|
a021ff6b22 | ||
|
|
6d1a90cac0 | ||
|
|
1f47c16102 | ||
|
|
abbcf60aed | ||
|
|
f339c882d7 | ||
|
|
982536e9e8 | ||
|
|
c17b351efb | ||
|
|
130bebf2c6 | ||
|
|
83c4ad2e59 | ||
|
|
0bcc6ed597 | ||
|
|
c61f854edc | ||
|
|
2998cf5e48 | ||
|
|
c777f8d97d | ||
|
|
7d4f5c8906 | ||
|
|
da39d07d48 | ||
|
|
b98f2456c0 | ||
|
|
564cc2b0bc | ||
|
|
49885c63c4 | ||
|
|
d7a6caa2ac | ||
|
|
73c383fd65 | ||
|
|
10b270f742 | ||
|
|
7a222923c7 | ||
|
|
430512dd27 | ||
|
|
d5ba15c23b | ||
|
|
037b43ee10 | ||
|
|
ab910403c6 | ||
|
|
8105437815 | ||
|
|
7b20cec035 | ||
|
|
8d512852a4 | ||
|
|
c8ad9b942a | ||
|
|
8153d4bb2a | ||
|
|
849dfee200 | ||
|
|
85540d96b6 | ||
|
|
7479974d79 | ||
|
|
3f1fb52fcb | ||
|
|
7e343d7006 | ||
|
|
72a5e1f695 | ||
|
|
253310bd1a | ||
|
|
fa6ccb08bd | ||
|
|
762507855e | ||
|
|
54610866f2 | ||
|
|
c39ff5c233 | ||
|
|
2ddc784965 | ||
|
|
10aabe8375 | ||
|
|
122647b39d | ||
|
|
02492c34a7 | ||
|
|
9436f604ba | ||
|
|
d9ca0deb08 | ||
|
|
0b985e8c35 | ||
|
|
c5d92ae02c | ||
|
|
94b60d9f70 | ||
|
|
b23eb8f29d | ||
|
|
3c44214d01 | ||
|
|
21f4cf7ab5 | ||
|
|
e94684aa39 | ||
|
|
a34cc48197 | ||
|
|
b262d91ccc | ||
|
|
39aa983771 | ||
|
|
5b9887dade | ||
|
|
c33402ce66 | ||
|
|
6f58f84151 | ||
|
|
6acb7caf5b | ||
|
|
8beff34cca | ||
|
|
478e0f74f7 | ||
|
|
b7bd23ab60 | ||
|
|
82533c1453 | ||
|
|
e0735b57ce | ||
|
|
1e0bfc3b0c | ||
|
|
cb0e89a38e | ||
|
|
da4d528463 | ||
|
|
394abbbe35 | ||
|
|
fd39bc8518 | ||
|
|
2663e8fba7 | ||
|
|
faebac6a77 | ||
|
|
bc1b09e997 | ||
|
|
af358f777b | ||
|
|
c0d27b4bfc | ||
|
|
7e50c95823 | ||
|
|
39068dda17 | ||
|
|
8185979ca4 | ||
|
|
7c44188130 | ||
|
|
c2d527bbd3 | ||
|
|
ac3ff095a1 | ||
|
|
0ed738cd61 | ||
|
|
6bbb7c8f7d | ||
|
|
d29429808c | ||
|
|
09eccd7cd9 | ||
|
|
bb2b8b4514 | ||
|
|
e20b9c5774 | ||
|
|
3badfa197a | ||
|
|
dee372e71b | ||
|
|
679be6e1bd | ||
|
|
92212fdd11 | ||
|
|
a6fb1ad9ef | ||
|
|
87d712fbd7 | ||
|
|
023809f099 | ||
|
|
ace37370d1 | ||
|
|
8efbd4fac1 | ||
|
|
06c8792887 | ||
|
|
3ea376a1b2 | ||
|
|
9667d30907 | ||
|
|
3f7ccc6c49 | ||
|
|
dd97ff5895 | ||
|
|
2e4d80d9bc | ||
|
|
1227dc5a2b | ||
|
|
ed828bc733 | ||
|
|
c25a018c05 | ||
|
|
266596af68 | ||
|
|
2c77b73ebc | ||
|
|
d81d4bbda3 | ||
|
|
400affe429 | ||
|
|
d3c63680e8 | ||
|
|
28de8a834c | ||
|
|
208374fc54 | ||
|
|
535a136a27 | ||
|
|
ba4c3e30a4 | ||
|
|
16d8a388cb | ||
|
|
5ea31b0b64 | ||
|
|
582c6d1c43 | ||
|
|
c63ae3f3af | ||
|
|
4c0df96a95 | ||
|
|
05c6b9379a | ||
|
|
fb7fdcd925 | ||
|
|
1774e2ad88 | ||
|
|
a402d9135e | ||
|
|
3d2c56d9ee | ||
|
|
f9308e6fed | ||
|
|
6710468020 | ||
|
|
ad1981fff6 | ||
|
|
01f9e71912 | ||
|
|
d41b75ee35 | ||
|
|
b829490aac | ||
|
|
7002bf8e34 | ||
|
|
625ea493fb | ||
|
|
79b3b2823b | ||
|
|
9be912e9fd | ||
|
|
3c3cd431cd | ||
|
|
8b8bab5c58 | ||
|
|
835fa6c41f | ||
|
|
8a6cf221a9 | ||
|
|
876563c492 | ||
|
|
be22c8547f | ||
|
|
82d98c4859 | ||
|
|
f1b5341f33 | ||
|
|
b3829493ea | ||
|
|
7db1253967 | ||
|
|
449db40d5f | ||
|
|
d5d0311bc6 | ||
|
|
0c4f01a892 | ||
|
|
bc7246f882 | ||
|
|
da65f3b016 | ||
|
|
a8c574219d | ||
|
|
a3751a77aa | ||
|
|
4f521e4dcb | ||
|
|
a9589d8d5b | ||
|
|
13e75aaf20 | ||
|
|
0c9bd8aaa0 | ||
|
|
5dba91c9ab | ||
|
|
7d6763cde7 | ||
|
|
dd1b23773e | ||
|
|
33253c0cfc | ||
|
|
0099c06056 | ||
|
|
1540660cc3 | ||
|
|
cff5e693d2 | ||
|
|
5159c1dc83 | ||
|
|
ccc7ad7cbd | ||
|
|
c8a61ca687 | ||
|
|
61e36d6aff | ||
|
|
e8c8742bae | ||
|
|
1cb93d76ed | ||
|
|
dadc939aab | ||
|
|
c59ea781e3 | ||
|
|
810ff62c26 | ||
|
|
5a0418bba6 | ||
|
|
baf506ae27 | ||
|
|
52ff03ae41 | ||
|
|
2d95edf8ab | ||
|
|
95b0fb81d6 | ||
|
|
eff65dce00 | ||
|
|
6c1c069261 | ||
|
|
4fe1e062f2 | ||
|
|
1fb0840e72 | ||
|
|
689ddf8bf0 | ||
|
|
d243ac49f3 | ||
|
|
de8f018b14 | ||
|
|
8407542600 | ||
|
|
a7a5cca8dd | ||
|
|
d9a70fd094 | ||
|
|
248cc0d3d3 | ||
|
|
2924d711cb | ||
|
|
d7db105a2f | ||
|
|
2ec2f45c82 | ||
|
|
a34769ae02 | ||
|
|
c0e4d805b1 | ||
|
|
6770336274 | ||
|
|
8d431dbb34 | ||
|
|
e8e7b83297 | ||
|
|
442340dcf2 | ||
|
|
91b037a335 | ||
|
|
d5ef3de64c | ||
|
|
167a0b0b29 | ||
|
|
954e3c70b2 | ||
|
|
b02a80abbd | ||
|
|
04313d3c3b | ||
|
|
fb8279f8f0 | ||
|
|
e0e56595c6 | ||
|
|
44d8cf9d4e | ||
|
|
282c1e53ec | ||
|
|
7ba98af1cc | ||
|
|
d3df5aaa52 | ||
|
|
1c83dcab5e | ||
|
|
6208081788 | ||
|
|
3795e92a82 | ||
|
|
0636123e7a | ||
|
|
69f9461bcd | ||
|
|
d1558a3472 | ||
|
|
8230596f98 | ||
|
|
cc4117e054 | ||
|
|
a0ddbc037f | ||
|
|
de82d4e616 | ||
|
|
fa220f9e93 | ||
|
|
aca112fa42 | ||
|
|
9f4077d35d | ||
|
|
9509b855f1 | ||
|
|
be72f4a046 | ||
|
|
8b36ce198f | ||
|
|
71de05dc68 | ||
|
|
83b5a9457a | ||
|
|
0b7ada9fd9 | ||
|
|
92bcef0b1c | ||
|
|
a10c4056d0 | ||
|
|
1fd3ee7149 | ||
|
|
e3a157bfe1 | ||
|
|
b446aa6590 | ||
|
|
c54ee71e1d | ||
|
|
1748a2ae12 | ||
|
|
eff46aa97a | ||
|
|
9fb186af75 | ||
|
|
f1b1001863 | ||
|
|
c5af536299 | ||
|
|
b9b2f691a5 | ||
|
|
bdc8817672 | ||
|
|
a55acb2816 | ||
|
|
d686c76db3 | ||
|
|
30c1ae651e | ||
|
|
adaad62fbd | ||
|
|
fe5ec205fc | ||
|
|
576400e0d9 | ||
|
|
f08a03106f | ||
|
|
f852b7789e | ||
|
|
b0bd06bdc5 | ||
|
|
84787f0ea2 | ||
|
|
f69b3dbbe6 | ||
|
|
ec5ec6f02c | ||
|
|
5d681e635b | ||
|
|
3deb65b529 | ||
|
|
3e527fee8b | ||
|
|
b1f1f94a76 | ||
|
|
43e140e6cc | ||
|
|
7ca9d445f1 | ||
|
|
90aaf71270 | ||
|
|
4f2570865c | ||
|
|
81556ec2e1 | ||
|
|
dd5a9c6067 | ||
|
|
982c50c756 | ||
|
|
3c7dc9b9ad | ||
|
|
8f4354936c | ||
|
|
e50941277f | ||
|
|
a72647b925 | ||
|
|
e254849009 | ||
|
|
5757f0e201 | ||
|
|
75fdf7adab | ||
|
|
702eafae4c | ||
|
|
4abb8cd87f | ||
|
|
5194361f3b | ||
|
|
bc83b75634 | ||
|
|
b1ff95affa | ||
|
|
a243d48fb1 | ||
|
|
937d09f1c3 | ||
|
|
3769092888 | ||
|
|
75281dee07 | ||
|
|
26a7c5eaef | ||
|
|
1e8254fabf | ||
|
|
43b6509b43 | ||
|
|
204c5b2446 | ||
|
|
7ab3347604 | ||
|
|
2f24a79d4d | ||
|
|
819b1fe0f6 | ||
|
|
9f04b2d56c | ||
|
|
5eff5fac67 | ||
|
|
d033d08c4d | ||
|
|
25644d061e | ||
|
|
e7372f22cb | ||
|
|
4530dd4fea | ||
|
|
072528af83 | ||
|
|
2c4d577f23 | ||
|
|
edd67e3473 | ||
|
|
f389642dba | ||
|
|
b8690c7f83 | ||
|
|
06b809a492 | ||
|
|
2ceda2faaa | ||
|
|
c7fc3afc21 | ||
|
|
af57043afd | ||
|
|
4a85f3660c | ||
|
|
69713f34b2 | ||
|
|
55801597c6 | ||
|
|
ff3cc98d46 | ||
|
|
79489796ae | ||
|
|
8e495494fd | ||
|
|
1abb716bb6 | ||
|
|
3f012dd7a3 | ||
|
|
bf6bab7963 | ||
|
|
9db10f56ef | ||
|
|
3b91e351b7 | ||
|
|
657937d272 | ||
|
|
d294fbab15 | ||
|
|
cfbf5624e1 | ||
|
|
c833e9a1a8 | ||
|
|
f1b261163b | ||
|
|
4553240601 | ||
|
|
007a4536c7 | ||
|
|
31cf5d4a5a | ||
|
|
3e3bd05c79 | ||
|
|
20deaf2950 | ||
|
|
680aef62ee | ||
|
|
f5eb4887a7 | ||
|
|
dc3452c5b7 | ||
|
|
a67efd1ad1 | ||
|
|
5dcbe79fa8 | ||
|
|
574ac9a603 | ||
|
|
6871444728 | ||
|
|
f4db7e3e53 | ||
|
|
da92b6bfb9 | ||
|
|
d713782fe1 | ||
|
|
02cde5f50b | ||
|
|
c5a7a83d3a | ||
|
|
6655a1a5e6 | ||
|
|
b8cb181070 | ||
|
|
a56471fe73 | ||
|
|
8c769812ae | ||
|
|
f7a842e4ee | ||
|
|
23c0334f68 | ||
|
|
e2ee7a0408 | ||
|
|
8f862b3df7 | ||
|
|
ae1f91a997 | ||
|
|
d4fb76770f | ||
|
|
ea28c791e6 | ||
|
|
251555f859 | ||
|
|
fa7bda7ee4 | ||
|
|
f385c4203a | ||
|
|
1e4243dedb | ||
|
|
5f78ee7b79 | ||
|
|
c6eb5c1785 | ||
|
|
11338b6382 | ||
|
|
6f3a074e00 | ||
|
|
e827079604 | ||
|
|
cf66db8d4b | ||
|
|
25acbf8501 | ||
|
|
e4c7a887d2 | ||
|
|
fb8a615660 | ||
|
|
1d9d49f406 | ||
|
|
0069b59ffe | ||
|
|
d4ba1b1e09 | ||
|
|
3a20b84f3a | ||
|
|
d52fc777ac | ||
|
|
5753a428d8 | ||
|
|
85afef5775 | ||
|
|
b4fc24995c | ||
|
|
5917bb10e4 | ||
|
|
258e150ebf | ||
|
|
96f2b2b617 | ||
|
|
d556db079b | ||
|
|
a811a82329 | ||
|
|
d17a0dae1f | ||
|
|
ef40f2f91b | ||
|
|
a921f9848c | ||
|
|
95ba3261fd | ||
|
|
fe81eb65c2 | ||
|
|
8428714cf5 | ||
|
|
bedf613cff | ||
|
|
e643ce5b99 | ||
|
|
cb64302294 | ||
|
|
8d5f2fec09 | ||
|
|
60e98235ca | ||
|
|
f55fb4055f | ||
|
|
da4e410bb7 | ||
|
|
cdd1f87437 | ||
|
|
7058373916 | ||
|
|
8dd38fd182 | ||
|
|
73479bab26 | ||
|
|
f5366c33bc | ||
|
|
db886163c2 | ||
|
|
91f5338db0 | ||
|
|
82a02287ac | ||
|
|
2dc674559e | ||
|
|
38e713fea2 | ||
|
|
2cbb14c36c | ||
|
|
610e88958e | ||
|
|
bb76477467 | ||
|
|
433a799759 | ||
|
|
22965ccce3 | ||
|
|
4257581f55 | ||
|
|
d60d629105 | ||
|
|
3f721b1717 | ||
|
|
97049bfab4 | ||
|
|
84944a87d3 | ||
|
|
fb62910b17 | ||
|
|
1cc0f5fee9 | ||
|
|
6896cf4258 | ||
|
|
188d63c6b8 | ||
|
|
cbc5d466f6 | ||
|
|
aef14a9f6d | ||
|
|
f48392064e | ||
|
|
994bdd0ca7 | ||
|
|
40ed070f21 | ||
|
|
f1e4b9937b | ||
|
|
0423388b52 | ||
|
|
096a9c5fc0 | ||
|
|
7518f74729 | ||
|
|
854401a150 | ||
|
|
a7a7efe9c3 | ||
|
|
31883f9adb | ||
|
|
3b04677f8f | ||
|
|
b5fb2b849a | ||
|
|
0c9f74ffa4 | ||
|
|
58da55da1e | ||
|
|
db64dea664 | ||
|
|
f7bff247aa | ||
|
|
edc08c46d4 | ||
|
|
78d83145ba | ||
|
|
0c81154f36 | ||
|
|
53fe08ea26 | ||
|
|
a6cb1a7052 | ||
|
|
c64fe7e45d | ||
|
|
a062135148 | ||
|
|
5ae7c15211 | ||
|
|
f29707fa9f | ||
|
|
e97d1e4a9a | ||
|
|
ed4fcc17b3 | ||
|
|
716eb14da1 | ||
|
|
f92489f99b | ||
|
|
ea3b3bc8a3 | ||
|
|
a80cb8f9ba | ||
|
|
d4813ba21c | ||
|
|
bf92008e45 | ||
|
|
9c73faaaeb | ||
|
|
f9bef8ecda | ||
|
|
302adb26d7 | ||
|
|
af606463ea | ||
|
|
c932f48a95 | ||
|
|
84c1f46ae4 | ||
|
|
b27e637894 | ||
|
|
4da9607b4d | ||
|
|
8abc22977c | ||
|
|
6f4be72785 | ||
|
|
0d2ca377df | ||
|
|
98f778c3bb | ||
|
|
9b9ae69404 | ||
|
|
1c747a10c8 | ||
|
|
c4354871f7 | ||
|
|
9f6450502c | ||
|
|
ae7fc5fe96 | ||
|
|
ec157f102b | ||
|
|
fbecbc1c82 |
4
.devcontainer/.dockerignore
Normal file
4
.devcontainer/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.dockerignore
|
||||||
|
devcontainer.json
|
||||||
|
Dockerfile
|
||||||
|
README.md
|
||||||
2
.devcontainer/Dockerfile
Normal file
2
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
FROM qmcgaw/godevcontainer:v0.20-alpine
|
||||||
|
RUN apk add wireguard-tools htop openssl
|
||||||
48
.devcontainer/README.md
Normal file
48
.devcontainer/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Development container
|
||||||
|
|
||||||
|
Development container that can be used with VSCode.
|
||||||
|
|
||||||
|
It works on Linux, Windows (WSL2) and OSX.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [VS code](https://code.visualstudio.com/download) 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
|
||||||
|
|
||||||
|
## 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 Docker on OSX**: 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. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
For any customization to take effect, you should "rebuild and reopen":
|
||||||
|
|
||||||
|
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||||
|
2. Select `Dev Containers: Rebuild Container`
|
||||||
|
|
||||||
|
Changes you can make are notably:
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Welcome script
|
||||||
|
{
|
||||||
|
"source": "/yourpath/.welcome.sh",
|
||||||
|
"target": "/root/.welcome.sh",
|
||||||
|
"type": "bind"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||||
@@ -1,115 +1,108 @@
|
|||||||
{
|
{
|
||||||
"name": "pia-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": "go mod download",
|
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||||
"workspaceFolder": "/workspace",
|
"capAdd": [
|
||||||
"extensions": [
|
"NET_ADMIN", // Gluetun specific
|
||||||
"golang.go",
|
"SYS_PTRACE" // for dlv Go debugging
|
||||||
"IBM.output-colorizer",
|
],
|
||||||
"eamodio.gitlens",
|
"securityOpt": [
|
||||||
"mhutchie.git-graph",
|
"seccomp=unconfined" // for dlv Go debugging
|
||||||
"davidanson.vscode-markdownlint",
|
],
|
||||||
"shardulm94.trailing-spaces",
|
"mounts": [
|
||||||
"alefragnani.Bookmarks",
|
// Zsh commands history persistence
|
||||||
"Gruntfuggly.todo-tree",
|
{
|
||||||
"mohsen1.prettify-json",
|
"source": "${localEnv:HOME}/.zsh_history",
|
||||||
"quicktype.quicktype",
|
"target": "/root/.zsh_history",
|
||||||
"spikespaz.vscode-smoothtype",
|
"type": "bind"
|
||||||
"stkb.rewrap",
|
},
|
||||||
"vscode-icons-team.vscode-icons"
|
// Git configuration file
|
||||||
],
|
{
|
||||||
"settings": {
|
"source": "${localEnv:HOME}/.gitconfig",
|
||||||
// General settings
|
"target": "/root/.gitconfig",
|
||||||
"files.eol": "\n",
|
"type": "bind"
|
||||||
// Docker
|
},
|
||||||
"remote.extensionKind": {
|
// SSH directory for Linux, OSX and WSL
|
||||||
"ms-azuretools.vscode-docker": "workspace"
|
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||||
},
|
// created in the container. On Windows, files are copied
|
||||||
// Golang general settings
|
// from /mnt/ssh to ~/.ssh to fix permissions.
|
||||||
"go.useLanguageServer": true,
|
{
|
||||||
"go.autocompleteUnimportedPackages": true,
|
"source": "${localEnv:HOME}/.ssh",
|
||||||
"go.gotoSymbol.includeImports": true,
|
"target": "/mnt/ssh",
|
||||||
"go.gotoSymbol.includeGoroot": true,
|
"type": "bind"
|
||||||
"gopls": {
|
},
|
||||||
"completeUnimported": true,
|
// Docker socket to access the host Docker server
|
||||||
"deepCompletion": true,
|
{
|
||||||
"usePlaceholders": false
|
"source": "/var/run/docker.sock",
|
||||||
},
|
"target": "/var/run/docker.sock",
|
||||||
"go.lintTool": "golangci-lint",
|
"type": "bind"
|
||||||
"go.lintFlags": [
|
}
|
||||||
"--fast",
|
],
|
||||||
"--enable",
|
"customizations": {
|
||||||
"staticcheck",
|
"vscode": {
|
||||||
"--enable",
|
"extensions": [
|
||||||
"bodyclose",
|
"golang.go",
|
||||||
"--enable",
|
"eamodio.gitlens", // IDE Git information
|
||||||
"dogsled",
|
"davidanson.vscode-markdownlint",
|
||||||
"--enable",
|
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||||
"gochecknoglobals",
|
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||||
"--enable",
|
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||||
"gochecknoinits",
|
"bierner.emojisense", // Emoji sense for markdown
|
||||||
"--enable",
|
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||||
"gocognit",
|
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||||
"--enable",
|
"github.vscode-pull-request-github", // Github interaction
|
||||||
"goconst",
|
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||||
"--enable",
|
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||||
"gocritic",
|
"IBM.output-colorizer", // Colorize your output/test logs
|
||||||
"--enable",
|
"github.copilot" // AI code completion
|
||||||
"gocyclo",
|
],
|
||||||
"--enable",
|
"settings": {
|
||||||
"golint",
|
"files.eol": "\n",
|
||||||
"--enable",
|
"remote.extensionKind": {
|
||||||
"gosec",
|
"ms-azuretools.vscode-docker": "workspace"
|
||||||
"--enable",
|
},
|
||||||
"interfacer",
|
"go.useLanguageServer": true,
|
||||||
"--enable",
|
"[go]": {
|
||||||
"maligned",
|
"editor.codeActionsOnSave": {
|
||||||
"--enable",
|
"source.organizeImports": "explicit"
|
||||||
"misspell",
|
}
|
||||||
"--enable",
|
},
|
||||||
"nakedret",
|
"[go.mod]": {
|
||||||
"--enable",
|
"editor.codeActionsOnSave": {
|
||||||
"prealloc",
|
"source.organizeImports": "explicit"
|
||||||
"--enable",
|
}
|
||||||
"scopelint",
|
},
|
||||||
"--enable",
|
"gopls": {
|
||||||
"unconvert",
|
"usePlaceholders": false,
|
||||||
"--enable",
|
"staticcheck": true,
|
||||||
"unparam",
|
"formatting.gofumpt": true,
|
||||||
"--enable",
|
},
|
||||||
"whitespace"
|
"go.lintTool": "golangci-lint",
|
||||||
],
|
"go.lintOnSave": "package",
|
||||||
// Golang on save
|
"editor.formatOnSave": true,
|
||||||
"go.buildOnSave": "workspace",
|
"go.buildTags": "linux",
|
||||||
"go.lintOnSave": "workspace",
|
"go.toolsEnvVars": {
|
||||||
"go.vetOnSave": "workspace",
|
"CGO_ENABLED": "0"
|
||||||
"editor.formatOnSave": true,
|
},
|
||||||
"[go]": {
|
"go.testEnvVars": {
|
||||||
"editor.codeActionsOnSave": {
|
"CGO_ENABLED": "1"
|
||||||
"source.organizeImports": true
|
},
|
||||||
}
|
"go.testFlags": [
|
||||||
},
|
"-v",
|
||||||
// Golang testing
|
"-race"
|
||||||
"go.toolsEnvVars": {
|
],
|
||||||
"GOFLAGS": "-tags=integration"
|
"go.testTimeout": "10s",
|
||||||
},
|
"go.coverOnSingleTest": true,
|
||||||
"gopls.env": {
|
"go.coverOnSingleTestFile": true,
|
||||||
"GOFLAGS": "-tags=integration"
|
"go.coverOnTestPackage": true
|
||||||
},
|
}
|
||||||
"go.testEnvVars": {},
|
}
|
||||||
"go.testFlags": [
|
}
|
||||||
"-v",
|
|
||||||
// "-race"
|
|
||||||
],
|
|
||||||
"go.testTimeout": "600s",
|
|
||||||
"go.coverOnSingleTestFile": true,
|
|
||||||
"go.coverOnSingleTest": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
|
|
||||||
services:
|
|
||||||
vscode:
|
|
||||||
image: qmcgaw/godevcontainer
|
|
||||||
volumes:
|
|
||||||
- ../:/workspace
|
|
||||||
- ~/.ssh:/home/vscode/.ssh:ro
|
|
||||||
- ~/.ssh:/root/.ssh:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
cap_add:
|
|
||||||
- SYS_PTRACE
|
|
||||||
security_opt:
|
|
||||||
- seccomp:unconfined
|
|
||||||
entrypoint: zsh -c "while sleep 1000; do :; done"
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
.devcontainer
|
.devcontainer
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
.vscode
|
|
||||||
cmd
|
|
||||||
!cmd/gluetun
|
|
||||||
doc
|
doc
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
title.svg
|
title.svg
|
||||||
|
|||||||
15
.github/CONTRIBUTING.md
vendored
15
.github/CONTRIBUTING.md
vendored
@@ -7,23 +7,12 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
|
|||||||
1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository
|
1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository
|
||||||
1. Create a new branch `git checkout -b my-branch-name`
|
1. Create a new branch `git checkout -b my-branch-name`
|
||||||
1. Modify the code
|
1. Modify the code
|
||||||
1. Ensure the docker build succeeds `docker build .`
|
1. Ensure the docker build succeeds `docker build .` (you might need `export DOCKER_BUILDKIT=1`)
|
||||||
1. Commit your modifications
|
1. Commit your modifications
|
||||||
1. Push to your fork and [submit a pull request](https://github.com/qdm12/gluetun/compare)
|
1. Push to your fork and [submit a pull request](https://github.com/qdm12/gluetun/compare)
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
- [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/)
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
Thanks for all the contributions, whether small or not so small!
|
|
||||||
|
|
||||||
- [@JeordyR](https://github.com/JeordyR) for testing the Mullvad version and opening a [PR with a few fixes](https://github.com/qdm12/gluetun/pull/84/files) 👍
|
|
||||||
- [@rorph](https://github.com/rorph) for a [PR to pick a random region for PIA](https://github.com/qdm12/gluetun/pull/70) and a [PR to make the container work with kubernetes](https://github.com/qdm12/gluetun/pull/69)
|
|
||||||
- [@JesterEE](https://github.com/JesterEE) for a [PR to fix silly line endings in block lists back then](https://github.com/qdm12/gluetun/pull/55) 📎
|
|
||||||
- [@elmerfdz](https://github.com/elmerfdz) for a [PR to add timezone information to have correct log timestampts](https://github.com/qdm12/gluetun/pull/51) 🕙
|
|
||||||
- [@Juggels](https://github.com/Juggels) for a [PR to write the PIA forwarded port to a file](https://github.com/qdm12/gluetun/pull/43)
|
|
||||||
- [@gdlx](https://github.com/gdlx) for a [PR to fix and improve PIA port forwarding script](https://github.com/qdm12/gluetun/pull/32)
|
|
||||||
- [@janaz](https://github.com/janaz) for keeping an eye on [updating things in the Dockerfile](https://github.com/qdm12/gluetun/pull/8)
|
|
||||||
|
|||||||
55
.github/ISSUE_TEMPLATE/bug.md
vendored
55
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug
|
|
||||||
about: Report a bug
|
|
||||||
title: 'Bug: ...'
|
|
||||||
labels: ":bug: bug"
|
|
||||||
assignees: qdm12
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TLDR**: *Describe your issue in a one liner here*
|
|
||||||
|
|
||||||
1. Is this urgent?
|
|
||||||
|
|
||||||
- [ ] Yes
|
|
||||||
- [x] No
|
|
||||||
|
|
||||||
2. What VPN service provider are you using?
|
|
||||||
|
|
||||||
- [x] PIA
|
|
||||||
- [ ] Mullvad
|
|
||||||
- [ ] Windscribe
|
|
||||||
- [ ] Surfshark
|
|
||||||
- [ ] Cyberghost
|
|
||||||
|
|
||||||
3. What's the version of the program?
|
|
||||||
|
|
||||||
**See the line at the top of your logs**
|
|
||||||
|
|
||||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
|
||||||
|
|
||||||
4. What are you using to run the container?
|
|
||||||
|
|
||||||
- [ ] Docker run
|
|
||||||
- [x] Docker Compose
|
|
||||||
- [ ] Kubernetes
|
|
||||||
- [ ] Docker stack
|
|
||||||
- [ ] Docker swarm
|
|
||||||
- [ ] Podman
|
|
||||||
- [ ] Other:
|
|
||||||
|
|
||||||
5. Extra information
|
|
||||||
|
|
||||||
Logs:
|
|
||||||
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration file:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Host OS:
|
|
||||||
117
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
117
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
name: Bug
|
||||||
|
description: Report a bug
|
||||||
|
title: "Bug: "
|
||||||
|
labels: [":bug: bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
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
|
||||||
|
id: urgent
|
||||||
|
attributes:
|
||||||
|
label: Is this urgent?
|
||||||
|
description: |
|
||||||
|
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/blob/main/setup/docker-image-tags.md) if that can help.
|
||||||
|
options:
|
||||||
|
- "No"
|
||||||
|
- "Yes"
|
||||||
|
- type: input
|
||||||
|
id: host-os
|
||||||
|
attributes:
|
||||||
|
label: Host OS
|
||||||
|
description: What is your host OS?
|
||||||
|
placeholder: "Debian Buster"
|
||||||
|
- type: dropdown
|
||||||
|
id: cpu-arch
|
||||||
|
attributes:
|
||||||
|
label: CPU arch
|
||||||
|
description: You can find it on Linux with `uname -m`.
|
||||||
|
options:
|
||||||
|
- x86_64
|
||||||
|
- aarch64
|
||||||
|
- armv7l
|
||||||
|
- "386"
|
||||||
|
- s390x
|
||||||
|
- ppc64le
|
||||||
|
- type: dropdown
|
||||||
|
id: vpn-service-provider
|
||||||
|
attributes:
|
||||||
|
label: VPN service provider
|
||||||
|
options:
|
||||||
|
- AirVPN
|
||||||
|
- Custom
|
||||||
|
- Cyberghost
|
||||||
|
- ExpressVPN
|
||||||
|
- FastestVPN
|
||||||
|
- Giganews
|
||||||
|
- HideMyAss
|
||||||
|
- IPVanish
|
||||||
|
- IVPN
|
||||||
|
- Mullvad
|
||||||
|
- NordVPN
|
||||||
|
- Privado
|
||||||
|
- Private Internet Access
|
||||||
|
- PrivateVPN
|
||||||
|
- ProtonVPN
|
||||||
|
- PureVPN
|
||||||
|
- SlickVPN
|
||||||
|
- Surfshark
|
||||||
|
- TorGuard
|
||||||
|
- VPNSecure.me
|
||||||
|
- VPNUnlimited
|
||||||
|
- VyprVPN
|
||||||
|
- WeVPN
|
||||||
|
- Windscribe
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: docker
|
||||||
|
attributes:
|
||||||
|
label: What are you using to run the container
|
||||||
|
options:
|
||||||
|
- docker run
|
||||||
|
- docker-compose
|
||||||
|
- Portainer
|
||||||
|
- Kubernetes
|
||||||
|
- Podman
|
||||||
|
- Unraid
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: What is the version of Gluetun
|
||||||
|
description: |
|
||||||
|
Copy paste the version line at the top of your logs.
|
||||||
|
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: "What's the problem 🤔"
|
||||||
|
placeholder: "That feature does not work..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Share your logs (at least 10 lines)
|
||||||
|
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
||||||
|
render: plain text
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: Share your configuration
|
||||||
|
description: Share your configuration such as `docker-compose.yml`. Ensure to remove credentials.
|
||||||
|
render: yml
|
||||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
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?
|
||||||
|
url: https://github.com/qdm12/gluetun/discussions/new/choose
|
||||||
|
about: Please create a Github discussion.
|
||||||
|
- name: Unraid template issue
|
||||||
|
url: https://github.com/qdm12/gluetun/discussions/550
|
||||||
|
about: Please read the relevant Github discussion.
|
||||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest a feature to add to this project
|
|
||||||
title: 'Feature request: ...'
|
|
||||||
labels: ":bulb: feature request"
|
|
||||||
assignees: qdm12
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
1. What's the feature?
|
|
||||||
|
|
||||||
2. Why do you need this feature?
|
|
||||||
|
|
||||||
3. Extra information?
|
|
||||||
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest a feature to add to Gluetun
|
||||||
|
title: "Feature request: "
|
||||||
|
labels: [":bulb: feature request"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: "What's the feature 🧐"
|
||||||
|
placeholder: "Make the tunnel resistant to earth quakes"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: extra
|
||||||
|
attributes:
|
||||||
|
label: "Extra information and references"
|
||||||
|
placeholder: |
|
||||||
|
- I tried `docker run something` and it doesn't work
|
||||||
|
- That [url](https://github.com/qdm12/gluetun) is interesting
|
||||||
55
.github/ISSUE_TEMPLATE/help.md
vendored
55
.github/ISSUE_TEMPLATE/help.md
vendored
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: Help
|
|
||||||
about: Ask for help
|
|
||||||
title: 'Help: ...'
|
|
||||||
labels: ":pray: help wanted"
|
|
||||||
assignees:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TLDR**: *Describe your issue in a one liner here*
|
|
||||||
|
|
||||||
1. Is this urgent?
|
|
||||||
|
|
||||||
- [ ] Yes
|
|
||||||
- [x] No
|
|
||||||
|
|
||||||
2. What VPN service provider are you using?
|
|
||||||
|
|
||||||
- [x] PIA
|
|
||||||
- [ ] Mullvad
|
|
||||||
- [ ] Windscribe
|
|
||||||
- [ ] Surfshark
|
|
||||||
- [ ] Cyberghost
|
|
||||||
|
|
||||||
3. What's the version of the program?
|
|
||||||
|
|
||||||
**See the line at the top of your logs**
|
|
||||||
|
|
||||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
|
||||||
|
|
||||||
4. What are you using to run the container?
|
|
||||||
|
|
||||||
- [ ] Docker run
|
|
||||||
- [x] Docker Compose
|
|
||||||
- [ ] Kubernetes
|
|
||||||
- [ ] Docker stack
|
|
||||||
- [ ] Docker swarm
|
|
||||||
- [ ] Podman
|
|
||||||
- [ ] Other:
|
|
||||||
|
|
||||||
5. Extra information
|
|
||||||
|
|
||||||
Logs:
|
|
||||||
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration file:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Host OS:
|
|
||||||
17
.github/ISSUE_TEMPLATE/provider.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/provider.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Support a VPN provider
|
||||||
|
about: Suggest a VPN provider to be supported
|
||||||
|
title: 'VPN provider support: NAME OF THE PROVIDER'
|
||||||
|
labels: ":bulb: New provider"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
One of the following is required:
|
||||||
|
|
||||||
|
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||||
|
- Publicly accessible URL to a structured (JSON etc.) 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's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
||||||
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
203
.github/labels.yml
vendored
203
.github/labels.yml
vendored
@@ -1,51 +1,152 @@
|
|||||||
- name: ":robot: bot"
|
- name: "Status: 🗯️ Waiting for feedback"
|
||||||
color: "69cde9"
|
color: "f7d692"
|
||||||
description: ""
|
- name: "Status: 🔴 Blocked"
|
||||||
- name: ":bug: bug"
|
color: "f7d692"
|
||||||
color: "b60205"
|
description: "Blocked by another issue or pull request"
|
||||||
description: ""
|
- name: "Status: 📌 Before next release"
|
||||||
- name: ":game_die: dependencies"
|
color: "f7d692"
|
||||||
color: "0366d6"
|
description: "Has to be done before the next release"
|
||||||
description: ""
|
- name: "Status: 🔒 After next release"
|
||||||
- name: ":memo: documentation"
|
color: "f7d692"
|
||||||
color: "c5def5"
|
description: "Will be done after the next release"
|
||||||
description: ""
|
- name: "Status: 🟡 Nearly resolved"
|
||||||
- name: ":busts_in_silhouette: duplicate"
|
color: "f7d692"
|
||||||
color: "cccccc"
|
description: "This might be resolved or is about to be resolved"
|
||||||
description: ""
|
|
||||||
- name: ":sparkles: enhancement"
|
- name: "Closed: ⚰️ Inactive"
|
||||||
color: "0054ca"
|
color: "959a9c"
|
||||||
description: ""
|
description: "No answer was received for weeks"
|
||||||
- name: ":bulb: feature request"
|
- name: "Closed: 👥 Duplicate"
|
||||||
color: "0e8a16"
|
color: "959a9c"
|
||||||
description: ""
|
description: "Issue duplicates an existing issue"
|
||||||
- name: ":mega: feedback"
|
- name: "Closed: 🗑️ Bad issue"
|
||||||
color: "03a9f4"
|
color: "959a9c"
|
||||||
description: ""
|
- name: "Closed: ☠️ cannot be done"
|
||||||
- name: ":rocket: future maybe"
|
color: "959a9c"
|
||||||
color: "fef2c0"
|
|
||||||
description: ""
|
- name: "Priority: 🚨 Urgent"
|
||||||
- name: ":hatching_chick: good first issue"
|
color: "03adfc"
|
||||||
color: "7057ff"
|
- name: "Priority: 💤 Low priority"
|
||||||
description: ""
|
color: "03adfc"
|
||||||
- name: ":pray: help wanted"
|
|
||||||
color: "4caf50"
|
- name: "Complexity: ☣️ Hard to do"
|
||||||
description: ""
|
color: "ff9efc"
|
||||||
- name: ":hand: hold"
|
- name: "Complexity: 🟩 Easy to do"
|
||||||
color: "24292f"
|
color: "ff9efc"
|
||||||
description: ""
|
|
||||||
- name: ":no_entry_sign: invalid"
|
- name: "Popularity: ❤️🔥 extreme"
|
||||||
color: "e6e6e6"
|
color: "ffc7ea"
|
||||||
description: ""
|
- name: "Popularity: ❤️ high"
|
||||||
- name: ":interrobang: maybe bug"
|
color: "ffc7ea"
|
||||||
color: "ff5722"
|
|
||||||
description: ""
|
# VPN providers
|
||||||
- name: ":thinking: needs more info"
|
- name: "☁️ AirVPN"
|
||||||
color: "795548"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ Custom"
|
||||||
- name: ":question: question"
|
color: "cfe8d4"
|
||||||
color: "3f51b5"
|
- name: "☁️ Cyberghost"
|
||||||
description: ""
|
color: "cfe8d4"
|
||||||
- name: ":coffin: wontfix"
|
- name: "☁️ Giganews"
|
||||||
color: "ffffff"
|
color: "cfe8d4"
|
||||||
description: ""
|
- name: "☁️ HideMyAss"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ IPVanish"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ IVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ ExpressVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ FastestVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Mullvad"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ NordVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Perfect Privacy"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ PIA"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Privado"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ PrivateVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ ProtonVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ PureVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ SlickVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Surfshark"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Torguard"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ VPNSecure.me"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ VPNUnlimited"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Vyprvpn"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ WeVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Windscribe"
|
||||||
|
color: "cfe8d4"
|
||||||
|
|
||||||
|
- name: "Category: User error 🤦"
|
||||||
|
from_name: "Category: Config problem 📝"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Healthcheck 🩺"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Documentation ✒️"
|
||||||
|
description: "A problem with the readme or a code comment."
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Maintenance ⛓️"
|
||||||
|
description: "Anything related to code or other maintenance"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Logs 📚"
|
||||||
|
description: "Something to change in logs"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Good idea 🎯"
|
||||||
|
description: "This is a good idea, judged by the maintainers"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Motivated! 🙌"
|
||||||
|
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Foolproof settings 👼"
|
||||||
|
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"
|
||||||
|
|||||||
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Docker build
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
paths-ignore:
|
|
||||||
- .devcontainer
|
|
||||||
- .github/ISSUE_TEMPLATE
|
|
||||||
- .github/workflows/buildx-release.yml
|
|
||||||
- .github/workflows/buildx-branch.yml
|
|
||||||
- .github/workflows/buildx-latest.yml
|
|
||||||
- .github/workflows/dockerhub-description.yml
|
|
||||||
- .github/workflows/labels.yml
|
|
||||||
- .github/workflows/misspell.yml
|
|
||||||
- .github/CODEOWNERS
|
|
||||||
- .github/CONTRIBUTING.md
|
|
||||||
- .github/FUNDING.yml
|
|
||||||
- .github/labels.yml
|
|
||||||
- .vscode
|
|
||||||
- cmd/ovpnparser
|
|
||||||
- cmd/resolver
|
|
||||||
- doc
|
|
||||||
- .gitignore
|
|
||||||
- docker-compose.yml
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
- title.svg
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Build image
|
|
||||||
run: docker build .
|
|
||||||
50
.github/workflows/buildx-branch.yml
vendored
50
.github/workflows/buildx-branch.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: Buildx branch
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
- '*/*'
|
|
||||||
- '!master'
|
|
||||||
paths-ignore:
|
|
||||||
- .devcontainer
|
|
||||||
- .github/ISSUE_TEMPLATE
|
|
||||||
- .github/workflows/build.yml
|
|
||||||
- .github/workflows/buildx-release.yml
|
|
||||||
- .github/workflows/buildx-latest.yml
|
|
||||||
- .github/workflows/dockerhub-description.yml
|
|
||||||
- .github/workflows/labels.yml
|
|
||||||
- .github/workflows/misspell.yml
|
|
||||||
- .github/CODEOWNERS
|
|
||||||
- .github/CONTRIBUTING.md
|
|
||||||
- .github/FUNDING.yml
|
|
||||||
- .github/labels.yml
|
|
||||||
- .vscode
|
|
||||||
- cmd/ovpnparser
|
|
||||||
- cmd/resolver
|
|
||||||
- doc
|
|
||||||
- .gitignore
|
|
||||||
- docker-compose.yml
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
- title.svg
|
|
||||||
jobs:
|
|
||||||
buildx:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Buildx setup
|
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
|
||||||
- name: Dockerhub login
|
|
||||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
|
||||||
- name: Run Buildx
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--progress plain \
|
|
||||||
--platform=linux/amd64 \
|
|
||||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
|
||||||
--build-arg COMMIT=`git rev-parse --short HEAD` \
|
|
||||||
--build-arg VERSION=${GITHUB_REF##*/} \
|
|
||||||
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
|
|
||||||
47
.github/workflows/buildx-latest.yml
vendored
47
.github/workflows/buildx-latest.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Buildx latest
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
paths-ignore:
|
|
||||||
- .devcontainer
|
|
||||||
- .github/ISSUE_TEMPLATE
|
|
||||||
- .github/workflows/build.yml
|
|
||||||
- .github/workflows/buildx-branch.yml
|
|
||||||
- .github/workflows/buildx-release.yml
|
|
||||||
- .github/workflows/dockerhub-description.yml
|
|
||||||
- .github/workflows/labels.yml
|
|
||||||
- .github/workflows/misspell.yml
|
|
||||||
- .github/CODEOWNERS
|
|
||||||
- .github/CONTRIBUTING.md
|
|
||||||
- .github/FUNDING.yml
|
|
||||||
- .github/labels.yml
|
|
||||||
- .vscode
|
|
||||||
- cmd/ovpnparser
|
|
||||||
- cmd/resolver
|
|
||||||
- doc
|
|
||||||
- .gitignore
|
|
||||||
- docker-compose.yml
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
- title.svg
|
|
||||||
jobs:
|
|
||||||
buildx:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Buildx setup
|
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
|
||||||
- name: Dockerhub login
|
|
||||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
|
||||||
- name: Run Buildx
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--progress plain \
|
|
||||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 \
|
|
||||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
|
||||||
--build-arg COMMIT=`git rev-parse --short HEAD` \
|
|
||||||
--build-arg VERSION=latest \
|
|
||||||
-t qmcgaw/private-internet-access:latest \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
|
|
||||||
47
.github/workflows/buildx-release.yml
vendored
47
.github/workflows/buildx-release.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Buildx release
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
paths-ignore:
|
|
||||||
- .devcontainer
|
|
||||||
- .github/ISSUE_TEMPLATE
|
|
||||||
- .github/workflows/build.yml
|
|
||||||
- .github/workflows/buildx-branch.yml
|
|
||||||
- .github/workflows/buildx-latest.yml
|
|
||||||
- .github/workflows/dockerhub-description.yml
|
|
||||||
- .github/workflows/labels.yml
|
|
||||||
- .github/workflows/misspell.yml
|
|
||||||
- .github/CODEOWNERS
|
|
||||||
- .github/CONTRIBUTING.md
|
|
||||||
- .github/FUNDING.yml
|
|
||||||
- .github/labels.yml
|
|
||||||
- .vscode
|
|
||||||
- cmd/ovpnparser
|
|
||||||
- cmd/resolver
|
|
||||||
- doc
|
|
||||||
- .gitignore
|
|
||||||
- docker-compose.yml
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
- title.svg
|
|
||||||
jobs:
|
|
||||||
buildx:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Buildx setup
|
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
|
||||||
- name: Dockerhub login
|
|
||||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
|
||||||
- name: Run Buildx
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--progress plain \
|
|
||||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 \
|
|
||||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
|
||||||
--build-arg COMMIT=`git rev-parse --short HEAD` \
|
|
||||||
--build-arg VERSION=${GITHUB_REF##*/} \
|
|
||||||
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
|
|
||||||
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
|
||||||
150
.github/workflows/ci.yml
vendored
Normal file
150
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- .github/workflows/ci.yml
|
||||||
|
- cmd/**
|
||||||
|
- internal/**
|
||||||
|
- pkg/**
|
||||||
|
- .dockerignore
|
||||||
|
- .golangci.yml
|
||||||
|
- Dockerfile
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/ci.yml
|
||||||
|
- cmd/**
|
||||||
|
- internal/**
|
||||||
|
- pkg/**
|
||||||
|
- .dockerignore
|
||||||
|
- .golangci.yml
|
||||||
|
- Dockerfile
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: reviewdog/action-misspell@v1
|
||||||
|
with:
|
||||||
|
locale: "US"
|
||||||
|
level: error
|
||||||
|
exclude: |
|
||||||
|
./internal/storage/servers.json
|
||||||
|
*.md
|
||||||
|
|
||||||
|
- name: Linting
|
||||||
|
run: docker build --target lint .
|
||||||
|
|
||||||
|
- name: Mocks check
|
||||||
|
run: docker build --target mocks .
|
||||||
|
|
||||||
|
- 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 --device /dev/net/tun \
|
||||||
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
|
test-container
|
||||||
|
|
||||||
|
- name: Build final image
|
||||||
|
run: docker build -t final-image .
|
||||||
|
|
||||||
|
codeql:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "^1.23"
|
||||||
|
- uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: go
|
||||||
|
- uses: github/codeql-action/autobuild@v3
|
||||||
|
- uses: github/codeql-action/analyze@v3
|
||||||
|
|
||||||
|
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@v4
|
||||||
|
|
||||||
|
# extract metadata (tags, labels) for Docker
|
||||||
|
# https://github.com/docker/metadata-action
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||||
|
images: |
|
||||||
|
ghcr.io/qdm12/gluetun
|
||||||
|
qmcgaw/gluetun
|
||||||
|
qmcgaw/private-internet-access
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
|
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
id: shortcommit
|
||||||
|
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
- name: Build and push final image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
CREATED=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||||
|
COMMIT=${{ steps.shortcommit.outputs.value }}
|
||||||
|
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
push: true
|
||||||
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@v4
|
||||||
|
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.
|
||||||
13
.github/workflows/configs/mlc-config.json
vendored
Normal file
13
.github/workflows/configs/mlc-config.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ignorePatterns": [
|
||||||
|
{
|
||||||
|
"pattern": "^https://console.substack.com/p/console-72$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeout": "20s",
|
||||||
|
"retryOn429": false,
|
||||||
|
"fallbackRetryDelay": "30s",
|
||||||
|
"aliveStatusCodes": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
}
|
||||||
19
.github/workflows/dockerhub-description.yml
vendored
19
.github/workflows/dockerhub-description.yml
vendored
@@ -1,19 +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
|
|
||||||
- name: Docker Hub Description
|
|
||||||
uses: peter-evans/dockerhub-description@v2.1.0
|
|
||||||
env:
|
|
||||||
DOCKERHUB_USERNAME: qmcgaw
|
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
DOCKERHUB_REPOSITORY: qmcgaw/private-internet-access
|
|
||||||
19
.github/workflows/labels.yml
vendored
19
.github/workflows/labels.yml
vendored
@@ -1,18 +1,17 @@
|
|||||||
name: labels
|
name: labels
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["master"]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- '.github/labels.yml'
|
- .github/labels.yml
|
||||||
- '.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:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v2
|
- uses: crazy-max/ghaction-github-labeler@v5
|
||||||
- name: Labeler
|
with:
|
||||||
if: success()
|
yaml-file: .github/labels.yml
|
||||||
uses: crazy-max/ghaction-github-labeler@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
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@v4
|
||||||
|
|
||||||
|
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||||
|
with:
|
||||||
|
globs: "**.md"
|
||||||
|
config: .markdownlint.json
|
||||||
|
|
||||||
|
- 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
|
||||||
16
.github/workflows/misspell.yml
vendored
16
.github/workflows/misspell.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
name: Misspells
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
jobs:
|
|
||||||
misspell:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: reviewdog/action-misspell@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
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@v4
|
||||||
|
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)
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
scratch.txt
|
||||||
@@ -1,47 +1,100 @@
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- err113
|
||||||
|
- containedctx
|
||||||
|
- maintidx
|
||||||
|
- path: "internal\\/server\\/.+\\.go"
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- 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:
|
||||||
|
- ireturn
|
||||||
|
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
|
||||||
enable:
|
enable:
|
||||||
|
# - cyclop
|
||||||
|
# - errorlint
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
- containedctx
|
||||||
|
- copyloopvar
|
||||||
|
- decorder
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- dupword
|
||||||
|
- durationcheck
|
||||||
|
- err113
|
||||||
|
- errchkjson
|
||||||
|
- errname
|
||||||
|
- exhaustive
|
||||||
|
- fatcontext
|
||||||
|
- forcetypeassert
|
||||||
|
- gci
|
||||||
|
- gocheckcompilerdirectives
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gocognit
|
- gocognit
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- gofumpt
|
||||||
|
- goheader
|
||||||
- goimports
|
- goimports
|
||||||
- golint
|
- gomoddirectives
|
||||||
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosmopolitan
|
||||||
- govet
|
- grouper
|
||||||
- ineffassign
|
- importas
|
||||||
- interfacer
|
- interfacebloat
|
||||||
- maligned
|
- intrange
|
||||||
|
- ireturn
|
||||||
|
- lll
|
||||||
|
- maintidx
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
- misspell
|
- misspell
|
||||||
|
- mnd
|
||||||
|
- musttag
|
||||||
- nakedret
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nosprintfhostport
|
||||||
|
- paralleltest
|
||||||
- prealloc
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- scopelint
|
- sqlclosecheck
|
||||||
- staticcheck
|
- tagalign
|
||||||
- structcheck
|
- tenv
|
||||||
- typecheck
|
- thelper
|
||||||
|
- tparallel
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- usestdlibvars
|
||||||
- varcheck
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
|
- zerologlint
|
||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- .devcontainer
|
|
||||||
- .github
|
|
||||||
- postgres
|
|
||||||
|
|||||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
13
.vscode/extensions.json
vendored
13
.vscode/extensions.json
vendored
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
// This list should be kept to the strict minimum
|
||||||
"shardulm94.trailing-spaces",
|
// to develop this project.
|
||||||
"ms-azuretools.vscode-docker",
|
"recommendations": [
|
||||||
"davidanson.vscode-markdownlint",
|
"golang.go",
|
||||||
"IBM.output-colorizer",
|
"davidanson.vscode-markdownlint",
|
||||||
"golang.go"
|
],
|
||||||
]
|
|
||||||
}
|
}
|
||||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Update a VPN provider servers data",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"program": "cmd/gluetun/main.go",
|
||||||
|
"args": [
|
||||||
|
"update",
|
||||||
|
"${input:updateMode}",
|
||||||
|
"-providers",
|
||||||
|
"${input:provider}"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
116
.vscode/settings.json
vendored
116
.vscode/settings.json
vendored
@@ -1,91 +1,29 @@
|
|||||||
{
|
{
|
||||||
// General settings
|
// The settings should be kept to the strict minimum
|
||||||
"files.eol": "\n",
|
// to develop this project.
|
||||||
// Docker
|
"files.eol": "\n",
|
||||||
"remote.extensionKind": {
|
"editor.formatOnSave": true,
|
||||||
"ms-azuretools.vscode-docker": "workspace"
|
"go.buildTags": "linux",
|
||||||
},
|
"go.toolsEnvVars": {
|
||||||
// Golang general settings
|
"CGO_ENABLED": "0"
|
||||||
"go.useLanguageServer": true,
|
},
|
||||||
"go.autocompleteUnimportedPackages": true,
|
"go.testEnvVars": {
|
||||||
"go.gotoSymbol.includeImports": true,
|
"CGO_ENABLED": "1"
|
||||||
"go.gotoSymbol.includeGoroot": true,
|
},
|
||||||
"gopls": {
|
"go.testFlags": [
|
||||||
"completeUnimported": true,
|
"-v",
|
||||||
"deepCompletion": true,
|
"-race"
|
||||||
"usePlaceholders": false
|
],
|
||||||
},
|
"go.testTimeout": "10s",
|
||||||
"go.lintTool": "golangci-lint",
|
"go.coverOnSingleTest": true,
|
||||||
"go.lintFlags": [
|
"go.coverOnSingleTestFile": true,
|
||||||
"--fast",
|
"go.coverOnTestPackage": true,
|
||||||
"--enable",
|
"go.useLanguageServer": true,
|
||||||
"rowserrcheck",
|
"[go]": {
|
||||||
"--enable",
|
"editor.codeActionsOnSave": {
|
||||||
"bodyclose",
|
"source.organizeImports": "explicit"
|
||||||
"--enable",
|
}
|
||||||
"dogsled",
|
},
|
||||||
"--enable",
|
"go.lintTool": "golangci-lint",
|
||||||
"dupl",
|
"go.lintOnSave": "package"
|
||||||
"--enable",
|
|
||||||
"gochecknoglobals",
|
|
||||||
"--enable",
|
|
||||||
"gochecknoinits",
|
|
||||||
"--enable",
|
|
||||||
"gocognit",
|
|
||||||
"--enable",
|
|
||||||
"goconst",
|
|
||||||
"--enable",
|
|
||||||
"gocritic",
|
|
||||||
"--enable",
|
|
||||||
"gocyclo",
|
|
||||||
"--enable",
|
|
||||||
"goimports",
|
|
||||||
"--enable",
|
|
||||||
"golint",
|
|
||||||
"--enable",
|
|
||||||
"gosec",
|
|
||||||
"--enable",
|
|
||||||
"interfacer",
|
|
||||||
"--enable",
|
|
||||||
"maligned",
|
|
||||||
"--enable",
|
|
||||||
"misspell",
|
|
||||||
"--enable",
|
|
||||||
"nakedret",
|
|
||||||
"--enable",
|
|
||||||
"prealloc",
|
|
||||||
"--enable",
|
|
||||||
"scopelint",
|
|
||||||
"--enable",
|
|
||||||
"unconvert",
|
|
||||||
"--enable",
|
|
||||||
"unparam",
|
|
||||||
"--enable",
|
|
||||||
"whitespace"
|
|
||||||
],
|
|
||||||
// Golang on save
|
|
||||||
"go.buildOnSave": "workspace",
|
|
||||||
"go.lintOnSave": "workspace",
|
|
||||||
"go.vetOnSave": "workspace",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"[go]": {
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Golang testing
|
|
||||||
"go.toolsEnvVars": {
|
|
||||||
"GOFLAGS": "-tags="
|
|
||||||
},
|
|
||||||
"gopls.env": {
|
|
||||||
"GOFLAGS": "-tags="
|
|
||||||
},
|
|
||||||
"go.testEnvVars": {},
|
|
||||||
"go.testFlags": [
|
|
||||||
"-v",
|
|
||||||
// "-race"
|
|
||||||
],
|
|
||||||
"go.testTimeout": "600s",
|
|
||||||
"go.coverOnSingleTestFile": true,
|
|
||||||
"go.coverOnSingleTest": true
|
|
||||||
}
|
}
|
||||||
279
Dockerfile
279
Dockerfile
@@ -1,82 +1,176 @@
|
|||||||
ARG ALPINE_VERSION=3.12
|
ARG ALPINE_VERSION=3.20
|
||||||
ARG GO_VERSION=1.15
|
ARG GO_ALPINE_VERSION=3.20
|
||||||
|
ARG GO_VERSION=1.23
|
||||||
|
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||||
|
ARG GOLANGCI_LINT_VERSION=v1.61.0
|
||||||
|
ARG MOCKGEN_VERSION=v1.6.0
|
||||||
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||||
RUN apk --update add git
|
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||||
|
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||||
|
|
||||||
|
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||||
|
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||||
|
# 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
|
||||||
ARG GOLANGCI_LINT_VERSION=v1.31.0
|
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
COPY --from=mockgen /bin /go/bin/mockgen
|
||||||
WORKDIR /tmp/gobuild
|
WORKDIR /tmp/gobuild
|
||||||
COPY .golangci.yml .
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
ARG VERSION=unknown
|
COPY cmd/ ./cmd/
|
||||||
ARG BUILD_DATE="an unknown date"
|
|
||||||
ARG COMMIT=unknown
|
|
||||||
COPY cmd/gluetun/main.go .
|
|
||||||
COPY internal/ ./internal/
|
COPY internal/ ./internal/
|
||||||
RUN go test ./...
|
|
||||||
|
FROM --platform=${BUILDPLATFORM} base AS test
|
||||||
|
# Note on the go race detector:
|
||||||
|
# - we set CGO_ENABLED=1 to have it enabled
|
||||||
|
# - we installed g++ to support the race detector
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
|
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||||
|
COPY .golangci.yml ./
|
||||||
RUN golangci-lint run --timeout=10m
|
RUN golangci-lint run --timeout=10m
|
||||||
RUN go build -trimpath -ldflags="-s -w \
|
|
||||||
-X 'main.version=$VERSION' \
|
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||||
-X 'main.buildDate=$BUILD_DATE' \
|
RUN git init && \
|
||||||
-X 'main.commit=$COMMIT' \
|
git config user.email ci@localhost && \
|
||||||
" -o entrypoint main.go
|
git config user.name ci && \
|
||||||
|
git config core.fileMode false && \
|
||||||
|
git add -A && \
|
||||||
|
git commit -m "snapshot" && \
|
||||||
|
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
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG VERSION=unknown
|
||||||
|
ARG CREATED="an unknown date"
|
||||||
|
ARG COMMIT=unknown
|
||||||
|
RUN GOARCH="$(xcputranslate translate -field arch -targetplatform ${TARGETPLATFORM})" \
|
||||||
|
GOARM="$(xcputranslate translate -field arm -targetplatform ${TARGETPLATFORM})" \
|
||||||
|
go build -trimpath -ldflags="-s -w \
|
||||||
|
-X 'main.version=$VERSION' \
|
||||||
|
-X 'main.created=$CREATED' \
|
||||||
|
-X 'main.commit=$COMMIT' \
|
||||||
|
" -o entrypoint cmd/gluetun/main.go
|
||||||
|
|
||||||
FROM alpine:${ALPINE_VERSION}
|
FROM alpine:${ALPINE_VERSION}
|
||||||
ARG VERSION=unknown
|
ARG VERSION=unknown
|
||||||
ARG BUILD_DATE="an unknown date"
|
ARG CREATED="an unknown date"
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT=unknown
|
||||||
LABEL \
|
LABEL \
|
||||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||||
org.opencontainers.image.created=$BUILD_DATE \
|
org.opencontainers.image.created=$CREATED \
|
||||||
org.opencontainers.image.version=$VERSION \
|
org.opencontainers.image.version=$VERSION \
|
||||||
org.opencontainers.image.revision=$COMMIT \
|
org.opencontainers.image.revision=$COMMIT \
|
||||||
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
|
||||||
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
|
||||||
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
||||||
org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \
|
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
|
||||||
org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS 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 \
|
||||||
VERSION_INFORMATION=on \
|
VPN_TYPE=openvpn \
|
||||||
PROTOCOL=udp \
|
# Common VPN options
|
||||||
|
VPN_INTERFACE=tun0 \
|
||||||
|
# OpenVPN
|
||||||
|
OPENVPN_ENDPOINT_IP= \
|
||||||
|
OPENVPN_ENDPOINT_PORT= \
|
||||||
|
OPENVPN_PROTOCOL=udp \
|
||||||
|
OPENVPN_USER= \
|
||||||
|
OPENVPN_PASSWORD= \
|
||||||
|
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
|
||||||
|
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
|
||||||
|
OPENVPN_VERSION=2.6 \
|
||||||
OPENVPN_VERBOSITY=1 \
|
OPENVPN_VERBOSITY=1 \
|
||||||
OPENVPN_ROOT=no \
|
OPENVPN_FLAGS= \
|
||||||
OPENVPN_TARGET_IP= \
|
OPENVPN_CIPHERS= \
|
||||||
OPENVPN_IPV6=off \
|
|
||||||
TZ= \
|
|
||||||
UID=1000 \
|
|
||||||
GID=1000 \
|
|
||||||
IP_STATUS_FILE="/tmp/gluetun/ip" \
|
|
||||||
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only
|
|
||||||
USER= \
|
|
||||||
PASSWORD= \
|
|
||||||
REGION= \
|
|
||||||
# PIA only
|
|
||||||
PIA_ENCRYPTION=strong \
|
|
||||||
PORT_FORWARDING=off \
|
|
||||||
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
|
||||||
# Mullvad and PureVPN only
|
|
||||||
COUNTRY= \
|
|
||||||
CITY= \
|
|
||||||
# Mullvad only
|
|
||||||
ISP= \
|
|
||||||
# Mullvad and Windscribe only
|
|
||||||
PORT= \
|
|
||||||
# Cyberghost only
|
|
||||||
CYBERGHOST_GROUP="Premium UDP Europe" \
|
|
||||||
# NordVPN only
|
|
||||||
SERVER_NUMBER= \
|
|
||||||
# Openvpn
|
|
||||||
OPENVPN_CIPHER= \
|
|
||||||
OPENVPN_AUTH= \
|
OPENVPN_AUTH= \
|
||||||
|
OPENVPN_PROCESS_USER=root \
|
||||||
|
OPENVPN_MSSFIX= \
|
||||||
|
OPENVPN_CUSTOM_CONFIG= \
|
||||||
|
# Wireguard
|
||||||
|
WIREGUARD_ENDPOINT_IP= \
|
||||||
|
WIREGUARD_ENDPOINT_PORT= \
|
||||||
|
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
|
||||||
|
SERVER_REGIONS= \
|
||||||
|
SERVER_COUNTRIES= \
|
||||||
|
SERVER_CITIES= \
|
||||||
|
SERVER_HOSTNAMES= \
|
||||||
|
SERVER_CATEGORIES= \
|
||||||
|
# # Mullvad only:
|
||||||
|
ISP= \
|
||||||
|
OWNED_ONLY=no \
|
||||||
|
# # Private Internet Access only:
|
||||||
|
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
||||||
|
VPN_PORT_FORWARDING=off \
|
||||||
|
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:
|
||||||
|
OPENVPN_CERT= \
|
||||||
|
OPENVPN_KEY= \
|
||||||
|
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
||||||
|
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:
|
||||||
|
SERVER_NUMBER= \
|
||||||
|
# # PIA only:
|
||||||
|
SERVER_NAMES= \
|
||||||
|
# # VPNUnlimited and ProtonVPN only:
|
||||||
|
STREAM_ONLY= \
|
||||||
|
FREE_ONLY= \
|
||||||
|
# # ProtonVPN only:
|
||||||
|
SECURE_CORE_ONLY= \
|
||||||
|
TOR_ONLY= \
|
||||||
|
# # Surfshark only:
|
||||||
|
MULTIHOP_ONLY= \
|
||||||
|
# # VPN Secure only:
|
||||||
|
PREMIUM_ONLY= \
|
||||||
|
# # PIA and ProtonVPN only:
|
||||||
|
PORT_FORWARD_ONLY= \
|
||||||
|
# Firewall
|
||||||
|
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||||
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
|
FIREWALL_INPUT_PORTS= \
|
||||||
|
FIREWALL_OUTBOUND_SUBNETS= \
|
||||||
|
FIREWALL_DEBUG=off \
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info \
|
||||||
|
# Health
|
||||||
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
|
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
||||||
|
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||||
|
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||||
# DNS over TLS
|
# DNS over TLS
|
||||||
DOT=on \
|
DOT=on \
|
||||||
DOT_PROVIDERS=cloudflare \
|
DOT_PROVIDERS=cloudflare \
|
||||||
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:0:0/96 \
|
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 \
|
||||||
DOT_VERBOSITY=1 \
|
|
||||||
DOT_VERBOSITY_DETAILS=0 \
|
|
||||||
DOT_VALIDATION_LOGLEVEL=0 \
|
|
||||||
DOT_CACHING=on \
|
DOT_CACHING=on \
|
||||||
DOT_IPV6=off \
|
DOT_IPV6=off \
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
@@ -84,33 +178,62 @@ ENV VPNSP=pia \
|
|||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
UNBLOCK= \
|
UNBLOCK= \
|
||||||
DNS_UPDATE_PERIOD=24h \
|
DNS_UPDATE_PERIOD=24h \
|
||||||
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
|
DNS_ADDRESS=127.0.0.1 \
|
||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
# Firewall
|
# HTTP proxy
|
||||||
FIREWALL=on \
|
HTTPPROXY= \
|
||||||
EXTRA_SUBNETS= \
|
HTTPPROXY_LOG=off \
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
||||||
FIREWALL_DEBUG=off \
|
HTTPPROXY_STEALTH=off \
|
||||||
# Tinyproxy
|
HTTPPROXY_USER= \
|
||||||
TINYPROXY=off \
|
HTTPPROXY_PASSWORD= \
|
||||||
TINYPROXY_LOG=Info \
|
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||||
TINYPROXY_PORT=8888 \
|
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
|
||||||
TINYPROXY_USER= \
|
|
||||||
TINYPROXY_PASSWORD= \
|
|
||||||
# Shadowsocks
|
# Shadowsocks
|
||||||
SHADOWSOCKS=off \
|
SHADOWSOCKS=off \
|
||||||
SHADOWSOCKS_LOG=off \
|
SHADOWSOCKS_LOG=off \
|
||||||
SHADOWSOCKS_PORT=8388 \
|
SHADOWSOCKS_LISTENING_ADDRESS=":8388" \
|
||||||
SHADOWSOCKS_PASSWORD= \
|
SHADOWSOCKS_PASSWORD= \
|
||||||
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \
|
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||||
UPDATER_PERIOD=0
|
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
||||||
ENTRYPOINT ["/entrypoint"]
|
# Control server
|
||||||
|
HTTP_CONTROL_SERVER_LOG=on \
|
||||||
|
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||||
|
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||||
|
# Server data updater
|
||||||
|
UPDATER_PERIOD=0 \
|
||||||
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
|
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||||
|
UPDATER_PROTONVPN_USERNAME= \
|
||||||
|
UPDATER_PROTONVPN_PASSWORD= \
|
||||||
|
# Public IP
|
||||||
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
|
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
|
||||||
|
VERSION_INFORMATION=on \
|
||||||
|
TZ= \
|
||||||
|
PUID= \
|
||||||
|
PGID=
|
||||||
|
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||||
HEALTHCHECK --interval=10m --timeout=10s --start-period=30s --retries=2 CMD /entrypoint healthcheck
|
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||||
RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tinyproxy tzdata && \
|
ARG TARGETPLATFORM
|
||||||
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \
|
RUN apk add --no-cache --update -l wget && \
|
||||||
|
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.5 && \
|
||||||
|
apk del openvpn && \
|
||||||
|
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
||||||
|
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 tinyproxy && \
|
|
||||||
deluser unbound && \
|
|
||||||
mkdir /gluetun
|
mkdir /gluetun
|
||||||
COPY --from=builder /tmp/gobuild/entrypoint /entrypoint
|
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
||||||
|
|||||||
517
README.md
517
README.md
@@ -1,388 +1,129 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
|
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||||
Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN and PureVPN VPN servers, using Go, OpenVPN,
|
|
||||||
iptables, DNS over TLS, ShadowSocks and Tinyproxy*
|

|
||||||
|
|
||||||
**ANNOUNCEMENT**: *Github Wiki reworked*
|
[](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
|
||||||
|
|
||||||
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
[](https://github.com/qdm12/gluetun/actions?query=workflow%3A%22Buildx+latest%22)
|
|
||||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/issues)
|

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

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

|
||||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|

|
||||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|
|
||||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
[](https://hub.docker.com/r/qmcgaw/gluetun/tags)
|
||||||
|
|
||||||
## Videos
|
[](https://github.com/qdm12/gluetun/commits/master)
|
||||||
|
[](https://github.com/qdm12/gluetun/graphs/contributors)
|
||||||
1. [**Introduction**](https://youtu.be/3jIbU6J2Hs0)
|
[](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
|
||||||
1. [**Connect a container**](https://youtu.be/mH7J_2JKNK0)
|
[](https://github.com/qdm12/gluetun/issues)
|
||||||
1. [**Connect LAN devices**](https://youtu.be/qvjrM15Y0uk)
|
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
|
|
||||||
## Features
|
[](https://github.com/qdm12/gluetun)
|
||||||
|

|
||||||
- Based on Alpine 3.12 for a small Docker image of 52MB
|

|
||||||
- Supports **Private Internet Access** (new and old), **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn**, **NordVPN** and **PureVPN** servers
|

|
||||||
- Supports Openvpn only for now
|
|
||||||
- DNS over TLS baked in with service provider(s) of your choice
|

|
||||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
|
||||||
- Choose the vpn network protocol, `udp` or `tcp`
|
## Quick links
|
||||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
|
||||||
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
- [Setup](#setup)
|
||||||
- Built in HTTP proxy (Tinyproxy, tunnels TCP)
|
- [Features](#features)
|
||||||
- [Connect other containers to it](https://github.com/qdm12/gluetun#connect-to-it)
|
- Problem?
|
||||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun#connect-to-it)
|
- 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)
|
||||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆
|
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||||
- VPN server side port forwarding for Private Internet Access and Vyprvpn
|
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
||||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
- Suggestion?
|
||||||
- Subprograms all drop root privileges once launched
|
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
||||||
- Subprograms output streams are all merged together
|
- Happy?
|
||||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||||
|
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||||
## Setup
|
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
||||||
|
- **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)
|
||||||
1. On some devices you may need to setup your tunnel kernel module on your host with `insmod /lib/modules/tun.ko` or `modprobe tun`
|
- Video:
|
||||||
- [Synology users Wiki page](https://github.com/qdm12/gluetun/wiki/Synology-setup)
|
|
||||||
1. Launch the container with:
|
[](https://youtu.be/0F6I03LQcI4)
|
||||||
|
|
||||||
```bash
|
- [Substack Console interview](https://console.substack.com/p/console-72)
|
||||||
docker run -d --name gluetun --cap-add=NET_ADMIN \
|
|
||||||
-e VPNSP="private internet access" -e REGION="CA Montreal" \
|
## Features
|
||||||
-e USER=js89ds7 -e PASSWORD=8fd9s239G \
|
|
||||||
-v /yourpath:/gluetun \
|
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||||
qmcgaw/private-internet-access
|
- 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
|
||||||
```
|
- Supports OpenVPN for all providers listed
|
||||||
|
- Supports Wireguard both kernelspace and userspace
|
||||||
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
|
- 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)
|
||||||
```bash
|
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
docker-compose up -d
|
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||||
```
|
- DNS over TLS baked in with service provider(s) of your choice
|
||||||
|
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
||||||
Note that you can:
|
- Choose the vpn network protocol, `udp` or `tcp`
|
||||||
|
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||||
- Change the many [environment variables](#environment-variables) available
|
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||||
- Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
|
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||||
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the Shadowsocks proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
|
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||||
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||||
|
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||||
**If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/gluetun/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)**
|
- 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)
|
||||||
|
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||||
1. You can update the image with `docker pull qmcgaw/private-internet-access:latest`. See the [wiki](https://github.com/qdm12/gluetun/wiki/Common-issues#use-a-release-tag) for more information on other tags available.
|
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||||
|
|
||||||
## Testing
|
## Setup
|
||||||
|
|
||||||
Check the VPN IP address matches your expectations
|
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
||||||
|
|
||||||
```sh
|
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||||
docker run --rm --network=container:gluetun alpine:3.12 wget -qO- https://ipinfo.io
|
|
||||||
```
|
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
|
||||||
|
|
||||||
▶ [Testing Wiki page](https://github.com/qdm12/gluetun/wiki/Testing-the-setup)
|
Here's a docker-compose.yml for the laziest:
|
||||||
|
|
||||||
## Environment variables
|
```yml
|
||||||
|
version: "3"
|
||||||
**TLDR**; only set the 🏁 marked environment variables to get started.
|
services:
|
||||||
|
gluetun:
|
||||||
### VPN
|
image: qmcgaw/gluetun
|
||||||
|
# container_name: gluetun
|
||||||
| Variable | Default | Choices | Description |
|
# line above must be uncommented to allow external containers to connect.
|
||||||
| --- | --- | --- | --- |
|
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
|
||||||
| 🏁 `VPNSP` | `private internet access` | `private internet access`, `private internet access old`, `mullvad`, `windscribe`, `surfshark`, `vyprvpn`, `nordvpn`, `purevpn` | VPN Service Provider |
|
cap_add:
|
||||||
| `IP_STATUS_FILE` | `/tmp/gluetun/ip` | Any filepath | Filepath to store the public IP address assigned |
|
- NET_ADMIN
|
||||||
| `PROTOCOL` | `udp` | `udp` or `tcp` | Network protocol to use |
|
devices:
|
||||||
| `OPENVPN_VERBOSITY` | `1` | `0` to `6` | Openvpn verbosity level |
|
- /dev/net/tun:/dev/net/tun
|
||||||
| `OPENVPN_ROOT` | `no` | `yes` or `no` | Run OpenVPN as root |
|
ports:
|
||||||
| `OPENVPN_TARGET_IP` | | Valid IP address | Specify a target VPN server (or gateway) IP address to use |
|
- 8888:8888/tcp # HTTP proxy
|
||||||
| `OPENVPN_CIPHER` | | i.e. `aes-256-gcm` | Specify a custom cipher to use. It will also set `ncp-disable` if using AES GCM for PIA |
|
- 8388:8388/tcp # Shadowsocks
|
||||||
| `OPENVPN_AUTH` | | i.e. `sha256` | Specify a custom auth algorithm to use |
|
- 8388:8388/udp # Shadowsocks
|
||||||
| `OPENVPN_IPV6` | `off` | `on`, `off` | Enable tunneling of IPv6 (only for Mullvad) |
|
volumes:
|
||||||
|
- /yourpath:/gluetun
|
||||||
*For all providers below, server location parameters are all optional. By default a random server is picked using the filter settings provided.*
|
environment:
|
||||||
|
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
||||||
- Private Internet Access
|
- VPN_SERVICE_PROVIDER=ivpn
|
||||||
|
- VPN_TYPE=openvpn
|
||||||
| Variable | Default | Choices | Description |
|
# OpenVPN:
|
||||||
| --- | --- | --- | --- |
|
- OPENVPN_USER=
|
||||||
| 🏁 `USER` | | | Your username |
|
- OPENVPN_PASSWORD=
|
||||||
| 🏁 `PASSWORD` | | | Your password |
|
# Wireguard:
|
||||||
| `REGION` | | One of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) | VPN server region |
|
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
||||||
| `PIA_ENCRYPTION` | `strong` | `normal`, `strong` | Encryption preset |
|
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||||
| `PORT_FORWARDING` | `off` | `on`, `off` | Enable port forwarding on the VPN server **for old only** |
|
# Timezone for accurate log times
|
||||||
| `PORT_FORWARDING_STATUS_FILE` | `/tmp/gluetun/forwarded_port` | Any filepath | Filepath to store the forwarded port number **for old only** |
|
- TZ=
|
||||||
|
# Server list updater
|
||||||
- Mullvad
|
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||||
|
- UPDATER_PERIOD=
|
||||||
| Variable | Default | Choices | Description |
|
```
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your user ID |
|
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||||
| `COUNTRY` | | One of the [Mullvad countries](https://mullvad.net/en/servers/#openvpn) | VPN server country |
|
|
||||||
| `CITY` | | One of the [Mullvad cities](https://mullvad.net/en/servers/#openvpn) | VPN server city |
|
## License
|
||||||
| `ISP` | | One of the [Mullvad ISP](https://mullvad.net/en/servers/#openvpn) | VPN server ISP |
|
|
||||||
| `PORT` | | `80`, `443` or `1401` for TCP; `53`, `1194`, `1195`, `1196`, `1197`, `1300`, `1301`, `1302`, `1303` or `1400` for UDP. Defaults to TCP `443` and UDP `1194` | Custom VPN port to use |
|
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
||||||
|
|
||||||
💡 [Mullvad IPv6 Wiki page](https://github.com/qdm12/gluetun/wiki/Mullvad-IPv6)
|
|
||||||
|
|
||||||
- Windscribe
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your username |
|
|
||||||
| 🏁 `PASSWORD` | | | Your password |
|
|
||||||
| `REGION` | | One of the [Windscribe regions](https://windscribe.com/status) | VPN server region |
|
|
||||||
| `PORT` | | One from the [this list of ports](https://windscribe.com/getconfig/openvpn) | Custom VPN port to use |
|
|
||||||
|
|
||||||
- Surfshark
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your **service** username, found at the bottom of the [manual setup page](https://account.surfshark.com/setup/manual) |
|
|
||||||
| 🏁 `PASSWORD` | | | Your **service** password |
|
|
||||||
| `REGION` | | One of the [Surfshark regions](https://github.com/qdm12/gluetun/wiki/Surfshark-Servers) | VPN server region |
|
|
||||||
|
|
||||||
- Cyberghost
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your username |
|
|
||||||
| 🏁 `PASSWORD` | | | Your password |
|
|
||||||
| 🏁 `CLIENT_KEY` | | | Your device client key content, **see below** |
|
|
||||||
| `REGION` | | One of the Cyberghost regions, [Wiki page](https://github.com/qdm12/gluetun/wiki/Cyberghost-Servers) | VPN server country |
|
|
||||||
| `CYBERGHOST_GROUP` | `Premium UDP Europe` | One of the server groups (see above Wiki page) | Server group |
|
|
||||||
|
|
||||||
To specify your client key, you can either:
|
|
||||||
|
|
||||||
- Bind mount it at `/files/client.key`, for example with `-v /yourpath/client.key:/files/client.key:ro`
|
|
||||||
- Convert it to a single line value using:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -it --rm -v /yourpath/client.key:/files/client.key:ro qmcgaw/private-internet-access clientkey
|
|
||||||
```
|
|
||||||
|
|
||||||
And use the line produced as the value for the environment variable `CLIENT_KEY`.
|
|
||||||
|
|
||||||
- Vyprvpn
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your username |
|
|
||||||
| 🏁 `PASSWORD` | | | Your password |
|
|
||||||
| `REGION` | | One of the [VyprVPN regions](https://www.vyprvpn.com/server-locations) | VPN server region |
|
|
||||||
|
|
||||||
- NordVPN
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your username |
|
|
||||||
| 🏁 `PASSWORD` | | | Your password |
|
|
||||||
| `REGION` | | One of the NordVPN server country, i.e. `Switzerland` | VPN server country |
|
|
||||||
| `SERVER_NUMBER` | | Server integer number | Optional server number. For example `251` for `Italy #251` |
|
|
||||||
|
|
||||||
- PureVPN
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 🏁 `USER` | | | Your user ID |
|
|
||||||
| 🏁 `REGION` | | One of the [PureVPN regions](https://support.purevpn.com/vpn-servers) | VPN server region |
|
|
||||||
| `COUNTRY` | | One of the [PureVPN countries](https://support.purevpn.com/vpn-servers) | VPN server country |
|
|
||||||
| `CITY` | | One of the [PureVPN cities](https://support.purevpn.com/vpn-servers) | VPN server city |
|
|
||||||
|
|
||||||
### DNS over TLS
|
|
||||||
|
|
||||||
None of the following values are required.
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `DOT` | `on` | `on`, `off` | Activate DNS over TLS with Unbound |
|
|
||||||
| `DOT_PROVIDERS` | `cloudflare` | `cloudflare`, `google`, `quad9`, `quadrant`, `cleanbrowsing`, `securedns`, `libredns` | Comma delimited list of DNS over TLS providers |
|
|
||||||
| `DOT_CACHING` | `on` | `on`, `off` | Unbound caching |
|
|
||||||
| `DOT_IPV6` | `off` | `on`, `off` | DNS IPv6 resolution |
|
|
||||||
| `DOT_PRIVATE_ADDRESS` | All private CIDRs ranges | | Comma separated list of CIDRs or single IP addresses Unbound won't resolve to. Note that the default setting prevents DNS rebinding |
|
|
||||||
| `DOT_VERBOSITY` | `1` | `0` to `5` | Unbound verbosity level |
|
|
||||||
| `DOT_VERBOSITY_DETAILS` | `0` | `0` to `4` | Unbound details verbosity level |
|
|
||||||
| `DOT_VALIDATION_LOGLEVEL` | `0` | `0` to `2` | Unbound validation log level |
|
|
||||||
| `DNS_UPDATE_PERIOD` | `24h` | i.e. `0`, `30s`, `5m`, `24h` | Period to update block lists and cryptographic files and restart Unbound. Set to `0` to deactivate updates |
|
|
||||||
| `BLOCK_MALICIOUS` | `on` | `on`, `off` | Block malicious hostnames and IPs with Unbound |
|
|
||||||
| `BLOCK_SURVEILLANCE` | `off` | `on`, `off` | Block surveillance hostnames and IPs with Unbound |
|
|
||||||
| `BLOCK_ADS` | `off` | `on`, `off` | Block ads hostnames and IPs with Unbound |
|
|
||||||
| `UNBLOCK` | |i.e. `domain1.com,x.domain2.co.uk` | Comma separated list of domain names to leave unblocked with Unbound |
|
|
||||||
| `DNS_PLAINTEXT_ADDRESS` | `1.1.1.1` | Any IP address | IP address to use as DNS resolver if `DOT` is `off` |
|
|
||||||
| `DNS_KEEP_NAMESERVER` | `off` | `on` or `off` | Keep the nameservers in /etc/resolv.conf untouched, but disabled DNS blocking features |
|
|
||||||
|
|
||||||
### Firewall
|
|
||||||
|
|
||||||
That one is important if you want to connect to the container from your LAN for example, using Shadowsocks or Tinyproxy.
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `FIREWALL` | `on` | `on` or `off` | Turn on or off the container built-in firewall. You should use it for **debugging purposes** only. |
|
|
||||||
| `EXTRA_SUBNETS` | | i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28` | Comma separated subnets allowed in the container firewall |
|
|
||||||
| `FIREWALL_VPN_INPUT_PORTS` | | i.e. `1000,8080` | Comma separated list of ports to allow from the VPN server side (useful for **vyprvpn** port forwarding) |
|
|
||||||
| `FIREWALL_DEBUG` | `off` | `on` or `off` | Prints every firewall related command. You should use it for **debugging purposes** only. |
|
|
||||||
|
|
||||||
### Shadowsocks
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `SHADOWSOCKS` | `off` | `on`, `off` | Enable the internal Shadowsocks proxy |
|
|
||||||
| `SHADOWSOCKS_LOG` | `off` | `on`, `off` | Enable logging |
|
|
||||||
| `SHADOWSOCKS_PORT` | `8388` | `1024` to `65535` | Internal port number for Shadowsocks to listen on |
|
|
||||||
| `SHADOWSOCKS_PASSWORD` | | | Password to use to connect to Shadowsocks |
|
|
||||||
| `SHADOWSOCKS_METHOD` | `chacha20-ietf-poly1305` | `chacha20-ietf-poly1305`, `aes-128-gcm`, `aes-256-gcm` | Method to use for Shadowsocks |
|
|
||||||
|
|
||||||
### Tinyproxy
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `TINYPROXY` | `off` | `on`, `off` | Enable the internal HTTP proxy tinyproxy |
|
|
||||||
| `TINYPROXY_LOG` | `Info` | `Info`, `Connect`, `Notice`, `Warning`, `Error`, `Critical` | Tinyproxy log level |
|
|
||||||
| `TINYPROXY_PORT` | `8888` | `1024` to `65535` | Internal port number for Tinyproxy to listen on |
|
|
||||||
| `TINYPROXY_USER` | | | Username to use to connect to Tinyproxy |
|
|
||||||
| `TINYPROXY_PASSWORD` | | | Password to use to connect to Tinyproxy |
|
|
||||||
|
|
||||||
### System
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `TZ` | | i.e. `Europe/London` | Specify a timezone to use to have correct log times |
|
|
||||||
| `UID` | `1000` | | User ID to run as non root and for ownership of files written |
|
|
||||||
| `GID` | `1000` | | Group ID to run as non root and for ownership of files written |
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
| Variable | Default | Choices | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. |
|
|
||||||
| `VERSION_INFORMATION` | `on` | `on`, `off` | Logs a message indicating if a newer version is available once the VPN is connected |
|
|
||||||
| `UPDATER_PERIOD` | `0` | Valid duration string such as `24h` | Period to update all VPN servers information in memory and to /gluetun/servers.json. Set to `0` to disable. This does a burst of DNS over TLS requests, which may be blocked if you set `BLOCK_MALICIOUS=on` for example. |
|
|
||||||
|
|
||||||
## Connect to it
|
|
||||||
|
|
||||||
There are various ways to achieve this, depending on your use case.
|
|
||||||
|
|
||||||
- <details><summary>Connect containers in the same docker-compose.yml as Gluetun</summary><p>
|
|
||||||
|
|
||||||
Add `network_mode: "service:gluetun"` to your *docker-compose.yml* (no need for `depends_on`)
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Connect other containers to Gluetun</summary><p>
|
|
||||||
|
|
||||||
Add `--network=container:gluetun` when launching the container, provided Gluetun is already running
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Connect containers from another docker-compose.yml</summary><p>
|
|
||||||
|
|
||||||
Add `network_mode: "container:gluetun"` to your *docker-compose.yml*, provided Gluetun is already running
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)</summary><p>
|
|
||||||
|
|
||||||
You might want to use Shadowsocks instead which tunnels UDP as well as TCP, whereas Tinyproxy only tunnels TCP.
|
|
||||||
|
|
||||||
1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
|
|
||||||
1. Ensure the Gluetun container is launched with:
|
|
||||||
- port `8888` published `-p 8888:8888/tcp`
|
|
||||||
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
|
|
||||||
1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
|
|
||||||
1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Connect LAN devices through the built-in *Shadowsocks* proxy (per app, system wide, etc.)</summary><p>
|
|
||||||
|
|
||||||
1. Setup a Shadowsocks proxy client, there is a list of [ShadowSocks clients for **all platforms**](https://shadowsocks.org/en/download/clients.html)
|
|
||||||
- **note** some clients do not tunnel UDP so your DNS queries will be done locally and not through Gluetun and its built in DNS over TLS
|
|
||||||
- Clients that support such UDP tunneling are, as far as I know:
|
|
||||||
- iOS: Potatso Lite
|
|
||||||
- OSX: ShadowsocksX
|
|
||||||
- Android: Shadowsocks by Max Lv
|
|
||||||
1. Ensure the Gluetun container is launched with:
|
|
||||||
- port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
|
|
||||||
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
|
|
||||||
1. With your Shadowsocks proxy client
|
|
||||||
- Enter the Docker host (i.e. `192.168.1.10`) as the server IP
|
|
||||||
- Enter port TCP (and UDP, if available) `8388` as the server port
|
|
||||||
- Use the password you have set with `SHADOWSOCKS_PASSWORD`
|
|
||||||
- Choose the encryption method/algorithm to the method you specified in `SHADOWSOCKS_METHOD`
|
|
||||||
1. If you set `SHADOWSOCKS_LOG` to `on`, (a lot) more information will be logged in the Docker logs
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Access ports of containers connected to Gluetun</summary><p>
|
|
||||||
|
|
||||||
In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to Gluetun,
|
|
||||||
publish ports `8000` and `9000` for the Gluetun container and access them as you would with any other container
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
- <details><summary>Access ports of containers connected to Gluetun, all in the same docker-compose.yml</summary><p>
|
|
||||||
|
|
||||||
In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to Gluetun, publish port `8000` and `9000` for the Gluetun container.
|
|
||||||
The docker-compose.yml file would look like:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
version: '3.7'
|
|
||||||
services:
|
|
||||||
gluetun:
|
|
||||||
image: qmcgaw/private-internet-access
|
|
||||||
container_name: gluetun
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
environment:
|
|
||||||
- USER=js89ds7
|
|
||||||
- PASSWORD=8fd9s239G
|
|
||||||
ports:
|
|
||||||
- 8000:8000/tcp
|
|
||||||
- 9000:9000/tcp
|
|
||||||
abc:
|
|
||||||
image: abc
|
|
||||||
container_name: abc
|
|
||||||
network_mode: "service:gluetun"
|
|
||||||
xyz:
|
|
||||||
image: xyz
|
|
||||||
container_name: xyz
|
|
||||||
network_mode: "service:gluetun"
|
|
||||||
```
|
|
||||||
|
|
||||||
</p></details>
|
|
||||||
|
|
||||||
## Private Internet Access port forwarding
|
|
||||||
|
|
||||||
Note that [not all regions support port forwarding](https://www.privateinternetaccess.com/helpdesk/kb/articles/how-do-i-enable-port-forwarding-on-my-vpn).
|
|
||||||
|
|
||||||
When `PORT_FORWARDING=on`, a port will be forwarded on the VPN server side and written to the file specified by `PORT_FORWARDING_STATUS_FILE=/forwarded_port`.
|
|
||||||
It can be useful to mount this file as a volume to read it from other containers, for example to configure a torrenting client.
|
|
||||||
|
|
||||||
You can also use the HTTP control server (see below) to get the port forwarded.
|
|
||||||
|
|
||||||
## HTTP control server
|
|
||||||
|
|
||||||
[Wiki page](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server)
|
|
||||||
|
|
||||||
## Development and contributing
|
|
||||||
|
|
||||||
- Contribute with code: start with [this Wiki page](https://github.com/qdm12/gluetun/wiki/Developement-setup)
|
|
||||||
- [The list of existing contributors 👍](https://github.com/qdm12/gluetun/blob/master/.github/CONTRIBUTING.md#Contributors)
|
|
||||||
- [Github workflows](https://github.com/qdm12/gluetun/actions) to know what's building
|
|
||||||
- [List of issues and feature requests](https://github.com/qdm12/gluetun/issues)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This repository is under an [MIT license](https://github.com/qdm12/gluetun/master/license)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Sponsor me on [Github](https://github.com/sponsors/qdm12), donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw) or subscribe to a VPN provider through one of my affiliate links:
|
|
||||||
|
|
||||||
[](https://github.com/sponsors/qdm12)
|
|
||||||
[](https://www.paypal.me/qmcgaw)
|
|
||||||
|
|
||||||
[](https://windscribe.com/?affid=mh7nyafu)
|
|
||||||
|
|
||||||
Feel also free to have a look at [the Kanban board](https://github.com/qdm12/gluetun/projects/1) and [contribute](#Development-and-contributing) to the code or the issues discussion.
|
|
||||||
|
|
||||||
Many thanks to @Frepke, @Ralph521, G. Mendez, M. Otmar Weber, J. Perez and A. Cooper for supporting me financially 🥇👍
|
|
||||||
|
|||||||
@@ -2,393 +2,603 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
|
_ "github.com/breml/rootcerts"
|
||||||
"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/command"
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||||
|
"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"
|
||||||
"github.com/qdm12/gluetun/internal/firewall"
|
"github.com/qdm12/gluetun/internal/firewall"
|
||||||
gluetunLogging "github.com/qdm12/gluetun/internal/logging"
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
|
"github.com/qdm12/gluetun/internal/httpproxy"
|
||||||
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
"github.com/qdm12/gluetun/internal/openvpn"
|
"github.com/qdm12/gluetun/internal/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/params"
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||||
|
"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/settings"
|
|
||||||
"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/tinyproxy"
|
"github.com/qdm12/gluetun/internal/tun"
|
||||||
"github.com/qdm12/gluetun/internal/updater"
|
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
||||||
versionpkg "github.com/qdm12/gluetun/internal/version"
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
"github.com/qdm12/golibs/command"
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
"github.com/qdm12/golibs/files"
|
"github.com/qdm12/gluetun/internal/vpn"
|
||||||
"github.com/qdm12/golibs/logging"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/golibs/network"
|
"github.com/qdm12/gosettings/reader/sources/env"
|
||||||
|
"github.com/qdm12/goshutdown"
|
||||||
|
"github.com/qdm12/goshutdown/goroutine"
|
||||||
|
"github.com/qdm12/goshutdown/group"
|
||||||
|
"github.com/qdm12/goshutdown/order"
|
||||||
|
"github.com/qdm12/gosplash"
|
||||||
|
"github.com/qdm12/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
version = "unknown"
|
version = "unknown"
|
||||||
commit = "unknown"
|
commit = "unknown"
|
||||||
buildDate = "an unknown date"
|
created = "an unknown date"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
buildInfo := models.BuildInformation{
|
||||||
os.Exit(_main(ctx, os.Args))
|
Version: version,
|
||||||
|
Commit: commit,
|
||||||
|
Created: created,
|
||||||
|
}
|
||||||
|
|
||||||
|
background := context.Background()
|
||||||
|
signalCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
ctx, cancel := context.WithCancel(background)
|
||||||
|
|
||||||
|
logger := log.New(log.SetLevel(log.LevelInfo))
|
||||||
|
|
||||||
|
args := os.Args
|
||||||
|
tun := tun.New()
|
||||||
|
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||||
|
netLinker := netlink.New(netLinkDebugLogger)
|
||||||
|
cli := cli.New()
|
||||||
|
cmder := command.New()
|
||||||
|
|
||||||
|
reader := reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{
|
||||||
|
secrets.New(logger),
|
||||||
|
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)
|
||||||
|
go func() {
|
||||||
|
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for OS signal or run error
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case receivedSignal := <-signalCh:
|
||||||
|
signal.Stop(signalCh)
|
||||||
|
fmt.Println("")
|
||||||
|
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||||
|
cancel()
|
||||||
|
case err = <-errorCh:
|
||||||
|
close(errorCh)
|
||||||
|
if err == nil { // expected exit such as healthcheck
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
logger.Error(err.Error())
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown timed sequence, and force exit on second OS signal
|
||||||
|
const shutdownGracePeriod = 5 * time.Second
|
||||||
|
timer := time.NewTimer(shutdownGracePeriod)
|
||||||
|
select {
|
||||||
|
case shutdownErr := <-errorCh:
|
||||||
|
timer.Stop()
|
||||||
|
if shutdownErr != nil {
|
||||||
|
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Shutdown successful")
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case <-timer.C:
|
||||||
|
logger.Warn("Shutdown timed out")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func _main(background context.Context, args []string) int { //nolint:gocognit,gocyclo
|
var errCommandUnknown = errors.New("command is unknown")
|
||||||
|
|
||||||
|
//nolint:gocognit,gocyclo,maintidx
|
||||||
|
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||||
|
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||||
|
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||||
|
cli clier,
|
||||||
|
) error {
|
||||||
if len(args) > 1 { // cli operation
|
if len(args) > 1 { // cli operation
|
||||||
var err error
|
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
err = cli.HealthCheck()
|
return cli.HealthCheck(ctx, reader, logger)
|
||||||
case "clientkey":
|
case "clientkey":
|
||||||
err = cli.ClientKey(args[2:])
|
return cli.ClientKey(args[2:])
|
||||||
case "openvpnconfig":
|
case "openvpnconfig":
|
||||||
err = cli.OpenvpnConfig()
|
return cli.OpenvpnConfig(logger, reader, netLinker)
|
||||||
case "update":
|
case "update":
|
||||||
err = cli.Update(args[2:])
|
return cli.Update(ctx, args[2:], logger)
|
||||||
|
case "format-servers":
|
||||||
|
return cli.FormatServers(args[2:])
|
||||||
|
case "genkey":
|
||||||
|
return cli.GenKey(args[2:])
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("command %q is unknown", args[1])
|
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(background)
|
|
||||||
defer cancel()
|
|
||||||
logger := createLogger()
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: 15 * time.Second}
|
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||||
client := network.NewClient(15 * time.Second)
|
|
||||||
// Create configurators
|
|
||||||
fileManager := files.NewFileManager()
|
|
||||||
alpineConf := alpine.NewConfigurator(fileManager)
|
|
||||||
ovpnConf := openvpn.NewConfigurator(logger, fileManager)
|
|
||||||
dnsConf := dns.NewConfigurator(logger, client, fileManager)
|
|
||||||
routingConf := routing.NewRouting(logger, fileManager)
|
|
||||||
firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager)
|
|
||||||
tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger)
|
|
||||||
streamMerger := command.NewStreamMerger()
|
|
||||||
|
|
||||||
paramsReader := params.NewReader(logger, fileManager)
|
|
||||||
fmt.Println(gluetunLogging.Splash(version, commit, buildDate))
|
|
||||||
|
|
||||||
printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){
|
|
||||||
"OpenVPN": ovpnConf.Version,
|
|
||||||
"Unbound": dnsConf.Version,
|
|
||||||
"IPtables": firewallConf.Version,
|
|
||||||
"TinyProxy": tinyProxyConf.Version,
|
|
||||||
})
|
|
||||||
|
|
||||||
allSettings, err := settings.GetAllSettings(paramsReader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return err
|
||||||
return 1
|
}
|
||||||
|
splashSettings := gosplash.Settings{
|
||||||
|
User: "qdm12",
|
||||||
|
Repository: "gluetun",
|
||||||
|
Emails: []string{"quentin.mcgaw@gmail.com"},
|
||||||
|
Version: buildInfo.Version,
|
||||||
|
Commit: buildInfo.Commit,
|
||||||
|
Created: buildInfo.Created,
|
||||||
|
Announcement: "All control server routes will become private by default after the v3.41.0 release",
|
||||||
|
AnnounceExp: announcementExp,
|
||||||
|
// Sponsor information
|
||||||
|
PaypalUser: "qmcgaw",
|
||||||
|
GithubSponsor: "qdm12",
|
||||||
|
}
|
||||||
|
for _, line := range gosplash.MakeLines(splashSettings) {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allSettings settings.Settings
|
||||||
|
err = allSettings.Read(reader, logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
allSettings.SetDefaults()
|
||||||
|
|
||||||
|
// Note: no need to validate minimal settings for the firewall:
|
||||||
|
// - global log level is parsed below
|
||||||
|
// - firewall Debug and Enabled are booleans parsed from source
|
||||||
|
logLevel, err := log.ParseLevel(allSettings.Log.Level)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("log level: %w", err)
|
||||||
|
}
|
||||||
|
logger.Patch(log.SetLevel(logLevel))
|
||||||
|
netLinker.PatchLoggerLevel(logLevel)
|
||||||
|
|
||||||
|
routingLogger := logger.New(log.SetComponent("routing"))
|
||||||
|
if *allSettings.Firewall.Debug { // To remove in v4
|
||||||
|
routingLogger.Patch(log.SetLevel(log.LevelDebug))
|
||||||
|
}
|
||||||
|
routingConf := routing.New(netLinker, routingLogger)
|
||||||
|
|
||||||
|
defaultRoutes, err := routingConf.DefaultRoutes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localNetworks, err := routingConf.LocalNetworks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *allSettings.Firewall.Enabled {
|
||||||
|
err = firewallConf.SetEnabled(ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.Info(allSettings.String())
|
|
||||||
|
|
||||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||||
storage := storage.New(logger)
|
storageLogger := logger.New(log.SetComponent("storage"))
|
||||||
const updateServerFile = true
|
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
||||||
allServers, err := storage.SyncServers(constants.GetAllServers(), updateServerFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return err
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should never change
|
ipv6Supported, err := netLinker.IsIPv6Supported()
|
||||||
uid, gid := allSettings.System.UID, allSettings.System.GID
|
|
||||||
|
|
||||||
err = alpineConf.CreateUser("nonrootuser", uid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
err = fileManager.SetOwnership("/etc/unbound", uid, gid)
|
|
||||||
|
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return err
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
err = fileManager.SetOwnership("/etc/tinyproxy", uid, gid)
|
|
||||||
|
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
|
||||||
|
pprofServer, err := pprof.New(allSettings.Pprof)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return fmt.Errorf("creating Pprof server: %w", err)
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if allSettings.Firewall.Debug {
|
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||||
firewallConf.SetDebug()
|
|
||||||
routingConf.SetDebug()
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
|
const clientTimeout = 15 * time.Second
|
||||||
if err != nil {
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
logger.Error(err)
|
// Create configurators
|
||||||
return 1
|
alpineConf := alpine.New()
|
||||||
}
|
ovpnConf := openvpn.New(
|
||||||
|
logger.New(log.SetComponent("openvpn configurator")),
|
||||||
|
cmder, puid, pgid)
|
||||||
|
|
||||||
localSubnet, err := routingConf.LocalSubnet()
|
err = printVersions(ctx, logger, []printVersionElement{
|
||||||
if err != nil {
|
{name: "Alpine", getVersion: alpineConf.Version},
|
||||||
logger.Error(err)
|
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||||
return 1
|
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||||
}
|
{name: "IPtables", getVersion: firewallConf.Version},
|
||||||
|
|
||||||
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet)
|
|
||||||
|
|
||||||
if err := ovpnConf.CheckTUN(); err != nil {
|
|
||||||
logger.Warn(err)
|
|
||||||
err = ovpnConf.CreateTUN()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelReadyCh, dnsReadyCh := make(chan struct{}), make(chan struct{})
|
|
||||||
signalTunnelReady := func() { tunnelReadyCh <- struct{}{} }
|
|
||||||
signalDNSReady := func() { dnsReadyCh <- struct{}{} }
|
|
||||||
defer close(tunnelReadyCh)
|
|
||||||
defer close(dnsReadyCh)
|
|
||||||
|
|
||||||
if allSettings.Firewall.Enabled {
|
|
||||||
err := firewallConf.SetEnabled(ctx, true) // disabled by default
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = firewallConf.SetAllowedSubnets(ctx, allSettings.Firewall.AllowedSubnets)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
|
|
||||||
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
|
|
||||||
go collectStreamLines(ctx, streamMerger, logger, signalTunnelReady)
|
|
||||||
|
|
||||||
openvpnLooper := openvpn.NewLooper(allSettings.VPNSP, allSettings.OpenVPN, uid, gid, allServers,
|
|
||||||
ovpnConf, firewallConf, logger, client, fileManager, streamMerger, cancel)
|
|
||||||
wg.Add(1)
|
|
||||||
// wait for restartOpenvpn
|
|
||||||
go openvpnLooper.Run(ctx, wg)
|
|
||||||
|
|
||||||
updaterOptions := updater.NewOptions("127.0.0.1")
|
|
||||||
updaterLooper := updater.NewLooper(updaterOptions, allSettings.UpdaterPeriod, allServers, storage, openvpnLooper.SetAllServers, httpClient, logger)
|
|
||||||
wg.Add(1)
|
|
||||||
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
|
||||||
go updaterLooper.Run(ctx, wg)
|
|
||||||
|
|
||||||
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, logger, streamMerger, uid, gid)
|
|
||||||
wg.Add(1)
|
|
||||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
|
||||||
go unboundLooper.Run(ctx, wg, signalDNSReady)
|
|
||||||
|
|
||||||
publicIPLooper := publicip.NewLooper(client, logger, fileManager, allSettings.System.IPStatusFilepath, allSettings.PublicIPPeriod, uid, gid)
|
|
||||||
wg.Add(1)
|
|
||||||
go publicIPLooper.Run(ctx, wg)
|
|
||||||
wg.Add(1)
|
|
||||||
go publicIPLooper.RunRestartTicker(ctx, wg)
|
|
||||||
publicIPLooper.SetPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker
|
|
||||||
|
|
||||||
tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, allSettings.TinyProxy, logger, streamMerger, uid, gid, defaultInterface)
|
|
||||||
restartTinyproxy := tinyproxyLooper.Restart
|
|
||||||
wg.Add(1)
|
|
||||||
go tinyproxyLooper.Run(ctx, wg)
|
|
||||||
|
|
||||||
shadowsocksLooper := shadowsocks.NewLooper(firewallConf, allSettings.ShadowSocks, logger, defaultInterface)
|
|
||||||
restartShadowsocks := shadowsocksLooper.Restart
|
|
||||||
wg.Add(1)
|
|
||||||
go shadowsocksLooper.Run(ctx, wg)
|
|
||||||
|
|
||||||
if allSettings.TinyProxy.Enabled {
|
|
||||||
restartTinyproxy()
|
|
||||||
}
|
|
||||||
if allSettings.ShadowSocks.Enabled {
|
|
||||||
restartShadowsocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go routeReadyEvents(ctx, wg, tunnelReadyCh, dnsReadyCh,
|
|
||||||
unboundLooper, updaterLooper, publicIPLooper, routingConf, logger, httpClient,
|
|
||||||
allSettings.VersionInformation, allSettings.OpenVPN.Provider.PortForwarding.Enabled, openvpnLooper.PortForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
httpServer := server.New("0.0.0.0:8000", logger, openvpnLooper, unboundLooper, updaterLooper)
|
|
||||||
wg.Add(1)
|
|
||||||
go httpServer.Run(ctx, wg)
|
|
||||||
|
|
||||||
// Start openvpn for the first time
|
|
||||||
openvpnLooper.Restart()
|
|
||||||
|
|
||||||
signalsCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signalsCh,
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
os.Interrupt,
|
|
||||||
)
|
|
||||||
shutdownErrorsCount := 0
|
|
||||||
select {
|
|
||||||
case signal := <-signalsCh:
|
|
||||||
logger.Warn("Caught OS signal %s, shutting down", signal)
|
|
||||||
cancel()
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Warn("context canceled, shutting down")
|
|
||||||
}
|
|
||||||
logger.Info("Clearing ip status file %s", allSettings.System.IPStatusFilepath)
|
|
||||||
if err := fileManager.Remove(string(allSettings.System.IPStatusFilepath)); err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
shutdownErrorsCount++
|
|
||||||
}
|
|
||||||
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
|
|
||||||
logger.Info("Clearing forwarded port status file %s", allSettings.OpenVPN.Provider.PortForwarding.Filepath)
|
|
||||||
if err := fileManager.Remove(string(allSettings.OpenVPN.Provider.PortForwarding.Filepath)); err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
shutdownErrorsCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const shutdownGracePeriod = 5 * time.Second
|
|
||||||
waiting, waited := context.WithTimeout(context.Background(), shutdownGracePeriod)
|
|
||||||
go func() {
|
|
||||||
defer waited()
|
|
||||||
wg.Wait()
|
|
||||||
}()
|
|
||||||
<-waiting.Done()
|
|
||||||
if waiting.Err() == context.DeadlineExceeded {
|
|
||||||
if shutdownErrorsCount > 0 {
|
|
||||||
logger.Warn("Shutdown had %d errors", shutdownErrorsCount)
|
|
||||||
}
|
|
||||||
logger.Warn("Shutdown timed out")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if shutdownErrorsCount > 0 {
|
|
||||||
logger.Warn("Shutdown had %d errors")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
logger.Info("Shutdown successful")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func createLogger() logging.Logger {
|
|
||||||
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func printVersions(ctx context.Context, logger logging.Logger, versionFunctions map[string]func(ctx context.Context) (string, error)) {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
for name, f := range versionFunctions {
|
|
||||||
version, err := f(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
} else {
|
|
||||||
logger.Info("%s version: %s", name, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger, logger logging.Logger, signalTunnelReady func()) {
|
|
||||||
// Blocking line merging paramsReader for all programs: openvpn, tinyproxy, unbound and shadowsocks
|
|
||||||
logger.Info("Launching standard output merger")
|
|
||||||
streamMerger.CollectLines(ctx, func(line string) {
|
|
||||||
line, level := gluetunLogging.PostProcessLine(line)
|
|
||||||
if line == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch level {
|
|
||||||
case logging.InfoLevel:
|
|
||||||
logger.Info(line)
|
|
||||||
case logging.WarnLevel:
|
|
||||||
logger.Warn(line)
|
|
||||||
case logging.ErrorLevel:
|
|
||||||
logger.Error(line)
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case line == "openvpn: Initialization Sequence Completed":
|
|
||||||
signalTunnelReady()
|
|
||||||
case line == "openvpn: TLS Error: TLS key negotiation failed to occur within 60 seconds (check your network connectivity)":
|
|
||||||
logger.Warn("This means that either...")
|
|
||||||
logger.Warn("1. The VPN server IP address you are trying to connect to is no longer valid, see https://github.com/qdm12/gluetun/wiki/Update-servers-information")
|
|
||||||
logger.Warn("2. The VPN server crashed, try changing region")
|
|
||||||
logger.Warn("3. Your Internet connection is not working, ensure it works")
|
|
||||||
logger.Warn("Feel free to create an issue at https://github.com/qdm12/gluetun/issues/new/choose")
|
|
||||||
}
|
|
||||||
}, func(err error) {
|
|
||||||
logger.Warn(err)
|
|
||||||
})
|
})
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, tunnelReadyCh, dnsReadyCh <-chan struct{},
|
logger.Info(allSettings.String())
|
||||||
unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper,
|
|
||||||
routing routing.Routing, logger logging.Logger, httpClient *http.Client,
|
for _, warning := range allSettings.Warnings() {
|
||||||
versionInformation, portForwardingEnabled bool, startPortForward func()) {
|
logger.Warn(warning)
|
||||||
defer wg.Done()
|
}
|
||||||
tickerWg := &sync.WaitGroup{}
|
|
||||||
// for linters only
|
const permission = fs.FileMode(0o644)
|
||||||
var restartTickerContext context.Context
|
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||||
var restartTickerCancel context.CancelFunc = func() {}
|
if err != nil {
|
||||||
for {
|
return err
|
||||||
select {
|
}
|
||||||
case <-ctx.Done():
|
err = os.MkdirAll("/gluetun", permission)
|
||||||
restartTickerCancel() // for linters only
|
if err != nil {
|
||||||
tickerWg.Wait()
|
return err
|
||||||
return
|
}
|
||||||
case <-tunnelReadyCh: // blocks until openvpn is connected
|
|
||||||
unboundLooper.Restart()
|
const defaultUsername = "nonrootuser"
|
||||||
restartTickerCancel() // stop previous restart tickers
|
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
||||||
tickerWg.Wait()
|
if err != nil {
|
||||||
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
|
return fmt.Errorf("creating user: %w", err)
|
||||||
tickerWg.Add(2)
|
}
|
||||||
go unboundLooper.RunRestartTicker(restartTickerContext, tickerWg)
|
if nonRootUsername != defaultUsername {
|
||||||
go updaterLooper.RunRestartTicker(restartTickerContext, tickerWg)
|
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
||||||
if portForwardingEnabled {
|
}
|
||||||
time.AfterFunc(5*time.Second, startPortForward)
|
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
||||||
}
|
|
||||||
defaultInterface, _, err := routing.DefaultRoute()
|
if err := routingConf.Setup(); err != nil {
|
||||||
if err != nil {
|
if strings.Contains(err.Error(), "operation not permitted") {
|
||||||
logger.Warn(err)
|
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||||
} else {
|
}
|
||||||
vpnGatewayIP, err := routing.VPNGatewayIP(defaultInterface)
|
return fmt.Errorf("setting up routing: %w", err)
|
||||||
if err != nil {
|
}
|
||||||
logger.Warn(err)
|
defer func() {
|
||||||
} else {
|
routingLogger.Info("routing cleanup...")
|
||||||
logger.Info("Gateway VPN IP address: %s", vpnGatewayIP)
|
if err := routingConf.TearDown(); err != nil {
|
||||||
}
|
routingLogger.Error("cannot teardown routing: " + err.Error())
|
||||||
}
|
}
|
||||||
case <-dnsReadyCh:
|
}()
|
||||||
publicIPLooper.Restart() // TODO do not restart if disabled
|
|
||||||
if !versionInformation {
|
if err := firewallConf.SetOutboundSubnets(ctx, allSettings.Firewall.OutboundSubnets); err != nil {
|
||||||
break
|
return err
|
||||||
}
|
}
|
||||||
message, err := versionpkg.GetMessage(version, commit, httpClient)
|
if err := routingConf.SetOutboundRoutes(allSettings.Firewall.OutboundSubnets); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
logger.Error(err)
|
}
|
||||||
break
|
|
||||||
}
|
err = routingConf.AddLocalRules(localNetworks)
|
||||||
logger.Info(message)
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding local rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tunDevice = "/dev/net/tun"
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("creating tun device: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, port := range allSettings.Firewall.InputPorts {
|
||||||
|
for _, defaultRoute := range defaultRoutes {
|
||||||
|
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // TODO move inside firewall?
|
||||||
|
|
||||||
|
// Shutdown settings
|
||||||
|
const totalShutdownTimeout = 3 * time.Second
|
||||||
|
const defaultShutdownTimeout = 400 * time.Millisecond
|
||||||
|
defaultShutdownOnSuccess := func(goRoutineName string) {
|
||||||
|
logger.Info(goRoutineName + ": terminated ✔️")
|
||||||
|
}
|
||||||
|
defaultShutdownOnFailure := func(goRoutineName string, err error) {
|
||||||
|
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
|
||||||
|
}
|
||||||
|
defaultGroupOptions := []group.Option{
|
||||||
|
group.OptionTimeout(defaultShutdownTimeout),
|
||||||
|
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
|
}
|
||||||
|
|
||||||
|
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||||
|
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||||
|
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
|
||||||
|
|
||||||
|
if *allSettings.Pprof.Enabled {
|
||||||
|
// TODO run in run loop so this can be patched at runtime
|
||||||
|
pprofReady := make(chan struct{})
|
||||||
|
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
|
||||||
|
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
||||||
|
go dnsLooper.Run(dnsCtx, dnsDone)
|
||||||
|
otherGroupHandler.Add(dnsHandler)
|
||||||
|
|
||||||
|
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||||
|
controlGroupHandler.Add(dnsTickerHandler)
|
||||||
|
|
||||||
|
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
||||||
|
logger.New(log.SetComponent("ip getter")))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating public ip loop: %w", err)
|
||||||
|
}
|
||||||
|
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting public ip loop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
|
|
||||||
|
unzipper := unzip.New(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, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||||
|
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||||
|
buildInfo, *allSettings.Version.Enabled)
|
||||||
|
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"vpn", goroutine.OptionTimeout(time.Second))
|
||||||
|
go vpnLooper.Run(vpnCtx, vpnDone)
|
||||||
|
|
||||||
|
updaterLooper := updater.NewLoop(allSettings.Updater,
|
||||||
|
providers, storage, httpClient, updaterLogger)
|
||||||
|
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
||||||
|
go updaterLooper.Run(updaterCtx, updaterDone)
|
||||||
|
tickersGroupHandler.Add(updaterHandler)
|
||||||
|
|
||||||
|
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
|
||||||
|
controlGroupHandler.Add(updaterTickerHandler)
|
||||||
|
|
||||||
|
httpProxyLooper := httpproxy.NewLoop(
|
||||||
|
logger.New(log.SetComponent("http proxy")),
|
||||||
|
allSettings.HTTPProxy)
|
||||||
|
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
|
||||||
|
otherGroupHandler.Add(httpProxyHandler)
|
||||||
|
|
||||||
|
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
|
||||||
|
logger.New(log.SetComponent("shadowsocks")))
|
||||||
|
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||||
|
otherGroupHandler.Add(shadowsocksHandler)
|
||||||
|
|
||||||
|
controlServerAddress := *allSettings.ControlServer.Address
|
||||||
|
controlServerLogging := *allSettings.ControlServer.Log
|
||||||
|
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
|
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||||
|
logger.New(log.SetComponent("http server")),
|
||||||
|
allSettings.ControlServer.AuthFilePath,
|
||||||
|
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||||
|
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)
|
||||||
|
|
||||||
|
healthLogger := logger.New(log.SetComponent("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",
|
||||||
|
order.OptionTimeout(totalShutdownTimeout),
|
||||||
|
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
|
order.OptionOnFailure(defaultShutdownOnFailure))
|
||||||
|
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||||
|
vpnHandler, otherGroupHandler)
|
||||||
|
|
||||||
|
// Start VPN for the first time in a blocking call
|
||||||
|
// until the VPN is launched
|
||||||
|
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
type printVersionElement struct {
|
||||||
|
name string
|
||||||
|
getVersion func(ctx context.Context) (version string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type infoer interface {
|
||||||
|
Info(s string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersions(ctx context.Context, logger infoer,
|
||||||
|
elements []printVersionElement,
|
||||||
|
) (err error) {
|
||||||
|
const timeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for _, element := range elements {
|
||||||
|
version, err := element.getVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting %s version: %w", element.name, err)
|
||||||
|
}
|
||||||
|
logger.Info(element.name + " version: " + version)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
1720
doc/logo.svg
Normal file
1720
doc/logo.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 62 KiB |
BIN
doc/logo_256.png
Normal file
BIN
doc/logo_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
doc/paypal.jpg
BIN
doc/paypal.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
BIN
doc/sponsors.jpg
BIN
doc/sponsors.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,41 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
services:
|
|
||||||
gluetun:
|
|
||||||
image: qmcgaw/private-internet-access
|
|
||||||
container_name: gluetun
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
network_mode: bridge
|
|
||||||
ports:
|
|
||||||
- 8888:8888/tcp # Tinyproxy
|
|
||||||
- 8388:8388/tcp # Shadowsocks
|
|
||||||
- 8388:8388/udp # Shadowsocks
|
|
||||||
- 8000:8000/tcp # Built-in HTTP control server
|
|
||||||
# command:
|
|
||||||
volumes:
|
|
||||||
- /yourpath:/gluetun
|
|
||||||
environment:
|
|
||||||
# More variables are available, see the readme table
|
|
||||||
- VPNSP=private internet access
|
|
||||||
|
|
||||||
# Timezone for accurate logs times
|
|
||||||
- TZ=
|
|
||||||
|
|
||||||
# All VPN providers
|
|
||||||
- USER=js89ds7
|
|
||||||
|
|
||||||
# All VPN providers but Mullvad
|
|
||||||
- PASSWORD=8fd9s239G
|
|
||||||
|
|
||||||
# Cyberghost only
|
|
||||||
- CLIENT_KEY=
|
|
||||||
|
|
||||||
# All VPN providers but Mullvad
|
|
||||||
- REGION=Austria
|
|
||||||
|
|
||||||
# Mullvad only
|
|
||||||
- COUNTRY=Sweden
|
|
||||||
|
|
||||||
# Allow for example your LAN, set to: 192.168.1.0/24
|
|
||||||
- EXTRA_SUBNETS=
|
|
||||||
restart: always
|
|
||||||
71
go.mod
71
go.mod
@@ -1,13 +1,68 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.15
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fatih/color v1.9.0
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/golang/mock v1.4.4
|
github.com/breml/rootcerts v0.2.19
|
||||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
github.com/fatih/color v1.18.0
|
||||||
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
|
github.com/golang/mock v1.6.0
|
||||||
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307
|
github.com/klauspost/compress v1.17.11
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/klauspost/pgzip v1.2.6
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed
|
github.com/pelletier/go-toml/v2 v2.2.3
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc8
|
||||||
|
github.com/qdm12/gosettings v0.4.4
|
||||||
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
|
github.com/qdm12/gosplash v0.2.0
|
||||||
|
github.com/qdm12/gotree v0.3.0
|
||||||
|
github.com/qdm12/log v0.1.0
|
||||||
|
github.com/qdm12/ss-server v0.6.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/ulikunitz/xz v0.5.11
|
||||||
|
github.com/vishvananda/netlink v1.2.1
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
|
golang.org/x/net v0.31.0
|
||||||
|
golang.org/x/sys v0.30.0
|
||||||
|
golang.org/x/text v0.22.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 (
|
||||||
|
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.0 // indirect
|
||||||
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
|
github.com/miekg/dns v1.1.62 // 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/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.0 // indirect
|
||||||
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.26.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
304
go.sum
304
go.sum
@@ -1,142 +1,192 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||||
|
github.com/breml/rootcerts v0.2.19/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.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
|
github.com/cloudflare/circl v1.6.0/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/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
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/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 h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
|
|
||||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
|
||||||
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
|
||||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
|
|
||||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 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/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307 h1:+LhVxIKpZgUM8ZcopIuc3Yjk+p76dWRdYLQiAA7caZM=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||||
|
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-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
||||||
|
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||||
|
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||||
|
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/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||||
|
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||||
|
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||||
|
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||||
|
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
||||||
|
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||||
|
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||||
|
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
|
||||||
|
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
||||||
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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
|
||||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
|
||||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
|
||||||
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
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 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
|
||||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
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-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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
|
||||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
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=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
|
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||||
|
|||||||
@@ -2,24 +2,20 @@ package alpine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os/user"
|
"os/user"
|
||||||
|
|
||||||
"github.com/qdm12/golibs/files"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configurator interface {
|
type Alpine struct {
|
||||||
CreateUser(username string, uid int) error
|
alpineReleasePath string
|
||||||
|
passwdPath string
|
||||||
|
lookupID func(uid string) (*user.User, error)
|
||||||
|
lookup func(username string) (*user.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type configurator struct {
|
func New() *Alpine {
|
||||||
fileManager files.FileManager
|
return &Alpine{
|
||||||
lookupUID func(uid string) (*user.User, error)
|
alpineReleasePath: "/etc/alpine-release",
|
||||||
lookupUser func(username string) (*user.User, error)
|
passwdPath: "/etc/passwd",
|
||||||
}
|
lookupID: user.LookupId,
|
||||||
|
lookup: user.Lookup,
|
||||||
func NewConfigurator(fileManager files.FileManager) Configurator {
|
|
||||||
return &configurator{
|
|
||||||
fileManager: fileManager,
|
|
||||||
lookupUID: user.LookupId,
|
|
||||||
lookupUser: user.Lookup,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,54 @@
|
|||||||
package alpine
|
package alpine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateUser creates a user in Alpine with the given UID
|
var ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
func (c *configurator) CreateUser(username string, uid int) error {
|
|
||||||
UIDStr := fmt.Sprintf("%d", uid)
|
// CreateUser creates a user in Alpine with the given UID.
|
||||||
u, err := c.lookupUID(UIDStr)
|
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||||
|
UIDStr := strconv.Itoa(uid)
|
||||||
|
u, err := a.lookupID(UIDStr)
|
||||||
_, unknownUID := err.(user.UnknownUserIdError)
|
_, unknownUID := err.(user.UnknownUserIdError)
|
||||||
if err != nil && !unknownUID {
|
if err != nil && !unknownUID {
|
||||||
return fmt.Errorf("cannot create user: %w", err)
|
return "", err
|
||||||
} else if u != nil {
|
|
||||||
if u.Username == username {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("user with ID %d exists with username %q instead of %q", uid, u.Username, username)
|
|
||||||
}
|
}
|
||||||
u, err = c.lookupUser(username)
|
|
||||||
|
if u != nil {
|
||||||
|
if u.Username == username {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return u.Username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = a.lookup(username)
|
||||||
_, unknownUsername := err.(user.UnknownUserError)
|
_, unknownUsername := err.(user.UnknownUserError)
|
||||||
if err != nil && !unknownUsername {
|
if err != nil && !unknownUsername {
|
||||||
return fmt.Errorf("cannot create user: %w", err)
|
return "", err
|
||||||
} else if u != nil {
|
|
||||||
return fmt.Errorf("cannot create user: user with name %s already exists for ID %s instead of %d", username, u.Uid, uid)
|
|
||||||
}
|
}
|
||||||
passwd, err := c.fileManager.ReadFile("/etc/passwd")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot create user: %w", err)
|
|
||||||
}
|
|
||||||
passwd = append(passwd, []byte(fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid))...)
|
|
||||||
|
|
||||||
if err := c.fileManager.WriteToFile("/etc/passwd", passwd); err != nil {
|
if u != nil {
|
||||||
return fmt.Errorf("cannot create user: %w", err)
|
return "", fmt.Errorf("%w: with name %s for ID %s instead of %d",
|
||||||
|
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid)
|
||||||
|
_, err = file.WriteString(s)
|
||||||
|
if err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, file.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
27
internal/alpine/version.go
Normal file
27
internal/alpine/version.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *Alpine) Version(context.Context) (version string, err error) {
|
||||||
|
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
version = strings.ReplaceAll(string(b), "\n", "")
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
7
internal/cli/ci.go
Normal file
7
internal/cli/ci.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func (c *CLI) CI(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,138 +1,11 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
type CLI struct {
|
||||||
"context"
|
repoServersPath string
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
"github.com/qdm12/gluetun/internal/params"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
|
||||||
"github.com/qdm12/gluetun/internal/updater"
|
|
||||||
"github.com/qdm12/golibs/files"
|
|
||||||
"github.com/qdm12/golibs/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ClientKey(args []string) error {
|
|
||||||
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
|
||||||
filepath := flagSet.String("path", "/files/client.key", "file path to the client.key file")
|
|
||||||
if err := flagSet.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileManager := files.NewFileManager()
|
|
||||||
data, err := fileManager.ReadFile(*filepath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s := string(data)
|
|
||||||
s = strings.ReplaceAll(s, "\n", "")
|
|
||||||
s = strings.ReplaceAll(s, "\r", "")
|
|
||||||
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
|
|
||||||
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
|
|
||||||
fmt.Println(s)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HealthCheck() error {
|
func New() *CLI {
|
||||||
client := &http.Client{Timeout: time.Second}
|
return &CLI{
|
||||||
response, err := client.Get("http://localhost:8000/health")
|
repoServersPath: "./internal/storage/servers.json",
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode == http.StatusOK {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("HTTP status code %s with message: %s", response.Status, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenvpnConfig() error {
|
|
||||||
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
paramsReader := params.NewReader(logger, files.NewFileManager())
|
|
||||||
allSettings, err := settings.GetAllSettings(paramsReader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
allServers, err := storage.New(logger).SyncServers(constants.GetAllServers(), false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers)
|
|
||||||
connections, err := providerConf.GetOpenVPNConnections(allSettings.OpenVPN.Provider.ServerSelection)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lines := providerConf.BuildConf(
|
|
||||||
connections,
|
|
||||||
allSettings.OpenVPN.Verbosity,
|
|
||||||
allSettings.System.UID,
|
|
||||||
allSettings.System.GID,
|
|
||||||
allSettings.OpenVPN.Root,
|
|
||||||
allSettings.OpenVPN.Cipher,
|
|
||||||
allSettings.OpenVPN.Auth,
|
|
||||||
allSettings.OpenVPN.Provider.ExtraConfigOptions,
|
|
||||||
)
|
|
||||||
fmt.Println(strings.Join(lines, "\n"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Update(args []string) error {
|
|
||||||
options := updater.Options{CLI: true}
|
|
||||||
var flushToFile bool
|
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
|
||||||
flagSet.BoolVar(&flushToFile, "file", false, "Write results to /gluetun/servers.json (for end users)")
|
|
||||||
flagSet.BoolVar(&options.Stdout, "stdout", false, "Write results to console to modify the program (for maintainers)")
|
|
||||||
flagSet.StringVar(&options.DNSAddress, "dns", "1.1.1.1", "DNS resolver address to use")
|
|
||||||
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
|
|
||||||
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
|
|
||||||
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
|
|
||||||
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
|
|
||||||
flagSet.BoolVar(&options.PIAold, "piaold", false, "Update Private Internet Access pre-summer 2020 servers")
|
|
||||||
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
|
|
||||||
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
|
|
||||||
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
|
|
||||||
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
|
|
||||||
if err := flagSet.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !flushToFile && !options.Stdout {
|
|
||||||
return fmt.Errorf("at least one of -file or -stdout must be specified")
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
storage := storage.New(logger)
|
|
||||||
const writeSync = false
|
|
||||||
currentServers, err := storage.SyncServers(constants.GetAllServers(), writeSync)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot update servers: %w", err)
|
|
||||||
}
|
|
||||||
updater := updater.New(options, httpClient, currentServers, logger)
|
|
||||||
allServers, err := updater.UpdateServers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if flushToFile {
|
|
||||||
if err := storage.FlushToFile(allServers); err != nil {
|
|
||||||
return fmt.Errorf("cannot update servers: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
internal/cli/clientkey.go
Normal file
37
internal/cli/clientkey.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CLI) ClientKey(args []string) error {
|
||||||
|
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(*filepath, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := string(data)
|
||||||
|
s = strings.ReplaceAll(s, "\n", "")
|
||||||
|
s = strings.ReplaceAll(s, "\r", "")
|
||||||
|
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
|
||||||
|
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
|
||||||
|
fmt.Println(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
113
internal/cli/formatservers.go
Normal file
113
internal/cli/formatservers.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||||
|
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
var format, output string
|
||||||
|
allProviders := providers.All()
|
||||||
|
allProviderFlags := make([]string, len(allProviders))
|
||||||
|
for i, provider := range allProviders {
|
||||||
|
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
titleCaser := cases.Title(language.English)
|
||||||
|
for _, provider := range allProviderFlags {
|
||||||
|
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
||||||
|
}
|
||||||
|
if err := flagSet.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note the format is validated by storage.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()
|
||||||
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, err := storage.Format(providerToFormat, format)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("formatting servers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = filepath.Clean(output)
|
||||||
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening output file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprint(file, formatted)
|
||||||
|
if err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return fmt.Errorf("writing to output file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("closing output file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
42
internal/cli/healthcheck.go
Normal file
42
internal/cli/healthcheck.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
|
||||||
|
// Extract the health server port from the configuration.
|
||||||
|
var config settings.Health
|
||||||
|
err = config.Read(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SetDefaults()
|
||||||
|
|
||||||
|
err = config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, port, err := net.SplitHostPort(config.ServerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = 10 * time.Second
|
||||||
|
httpClient := &http.Client{Timeout: timeout}
|
||||||
|
client := healthcheck.NewClient(httpClient)
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
url := "http://127.0.0.1:" + port
|
||||||
|
return client.Check(ctx, url)
|
||||||
|
}
|
||||||
9
internal/cli/interfaces.go
Normal file
9
internal/cli/interfaces.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
|
||||||
|
type Source interface {
|
||||||
|
Read() (settings settings.Settings, err error)
|
||||||
|
ReadHealth() (health settings.Health, err error)
|
||||||
|
String() string
|
||||||
|
}
|
||||||
9
internal/cli/nooplogger.go
Normal file
9
internal/cli/nooplogger.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
type noopLogger struct{}
|
||||||
|
|
||||||
|
func newNoopLogger() *noopLogger {
|
||||||
|
return new(noopLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Info(string) {}
|
||||||
92
internal/cli/openvpnconfig.go
Normal file
92
internal/cli/openvpnconfig.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
"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/storage"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenvpnConfigLogger interface {
|
||||||
|
Info(s string)
|
||||||
|
Warn(s string)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := providerConf.OpenVPNConfig(connection,
|
||||||
|
allSettings.VPN.OpenVPN, ipv6Supported)
|
||||||
|
|
||||||
|
fmt.Println(strings.Join(lines, "\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
123
internal/cli/update.go
Normal file
123
internal/cli/update.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"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/updater"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||||
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||||
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||||
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdaterLogger interface {
|
||||||
|
Info(s string)
|
||||||
|
Warn(s string)
|
||||||
|
Error(s string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
|
options := settings.Updater{}
|
||||||
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
|
var csvProviders, ipToken, protonUsername, protonPassword string
|
||||||
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
|
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
|
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
||||||
|
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.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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !endUserMode && !maintainerMode {
|
||||||
|
return fmt.Errorf("%w", ErrModeUnspecified)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateAll {
|
||||||
|
options.Providers = providers.All()
|
||||||
|
} else {
|
||||||
|
if csvProviders == "" {
|
||||||
|
return fmt.Errorf("%w", ErrNoProviderSpecified)
|
||||||
|
}
|
||||||
|
options.Providers = strings.Split(csvProviders, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||||
|
options.ProtonUsername = &protonUsername
|
||||||
|
options.ProtonPassword = &protonPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
options.SetDefaults(options.Providers[0])
|
||||||
|
|
||||||
|
err := options.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("options validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientTimeout = 10 * time.Second
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||||
|
}
|
||||||
|
ipFetcher := api.NewResilient(fetchers, logger)
|
||||||
|
|
||||||
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
err := storage.FlushToFile(c.repoServersPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
5
internal/cli/warner.go
Normal file
5
internal/cli/warner.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
type Warner interface {
|
||||||
|
Warn(s string)
|
||||||
|
}
|
||||||
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
25
internal/configuration/settings/deprecated.go
Normal file
25
internal/configuration/settings/deprecated.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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.",
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
101
internal/configuration/settings/dns.go
Normal file
101
internal/configuration/settings/dns.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS contains settings to configure DNS.
|
||||||
|
type DNS struct {
|
||||||
|
// ServerAddress is the DNS server to use inside
|
||||||
|
// the Go program and for the system.
|
||||||
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
|
// DoT server. It cannot be the zero value in the internal
|
||||||
|
// state.
|
||||||
|
ServerAddress netip.Addr
|
||||||
|
// KeepNameserver is true if the existing DNS server
|
||||||
|
// found in /etc/resolv.conf should be used
|
||||||
|
// Note setting this to true will likely DNS traffic
|
||||||
|
// 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 over TLS server and the
|
||||||
|
// `ServerAddress` field will be ignored.
|
||||||
|
// It defaults to false and cannot be nil in the
|
||||||
|
// internal state.
|
||||||
|
KeepNameserver *bool
|
||||||
|
// DOT contains settings to configure the DoT
|
||||||
|
// server.
|
||||||
|
DoT DoT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNS) validate() (err error) {
|
||||||
|
err = d.DoT.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validating DoT settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
|
return DNS{
|
||||||
|
ServerAddress: d.ServerAddress,
|
||||||
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
|
DoT: d.DoT.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
|
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
|
d.DoT.overrideWith(other.DoT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNS) setDefaults() {
|
||||||
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||||
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
|
d.DoT.setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNS) String() string {
|
||||||
|
return d.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||||
|
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.AppendNode(d.DoT.toLinesNode())
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.DoT.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
195
internal/configuration/settings/dnsblacklist.go
Normal file
195
internal/configuration/settings/dnsblacklist.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSBlacklist is settings for the DNS blacklist building.
|
||||||
|
type DNSBlacklist struct {
|
||||||
|
BlockMalicious *bool
|
||||||
|
BlockAds *bool
|
||||||
|
BlockSurveillance *bool
|
||||||
|
AllowedHosts []string
|
||||||
|
AddBlockedHosts []string
|
||||||
|
AddBlockedIPs []netip.Addr
|
||||||
|
AddBlockedIPPrefixes []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DNSBlacklist) setDefaults() {
|
||||||
|
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
|
||||||
|
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
|
||||||
|
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 (
|
||||||
|
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||||
|
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b DNSBlacklist) validate() (err error) {
|
||||||
|
for _, host := range b.AllowedHosts {
|
||||||
|
if !hostRegex.MatchString(host) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, host := range b.AddBlockedHosts {
|
||||||
|
if !hostRegex.MatchString(host) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||||
|
return DNSBlacklist{
|
||||||
|
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||||
|
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||||
|
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||||
|
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||||
|
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||||
|
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||||
|
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||||
|
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
|
||||||
|
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
|
||||||
|
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
|
||||||
|
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
|
||||||
|
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||||
|
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||||
|
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||||
|
settings blockbuilder.Settings,
|
||||||
|
) {
|
||||||
|
return blockbuilder.Settings{
|
||||||
|
Client: client,
|
||||||
|
BlockMalicious: b.BlockMalicious,
|
||||||
|
BlockAds: b.BlockAds,
|
||||||
|
BlockSurveillance: b.BlockSurveillance,
|
||||||
|
AllowedHosts: b.AllowedHosts,
|
||||||
|
AddBlockedHosts: b.AddBlockedHosts,
|
||||||
|
AddBlockedIPs: b.AddBlockedIPs,
|
||||||
|
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b DNSBlacklist) String() string {
|
||||||
|
return b.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("DNS filtering settings:")
|
||||||
|
|
||||||
|
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
|
||||||
|
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
|
||||||
|
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
||||||
|
|
||||||
|
if len(b.AllowedHosts) > 0 {
|
||||||
|
allowedHostsNode := node.Append("Allowed hosts:")
|
||||||
|
for _, host := range b.AllowedHosts {
|
||||||
|
allowedHostsNode.Append(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.AddBlockedHosts) > 0 {
|
||||||
|
blockedHostsNode := node.Append("Blocked hosts:")
|
||||||
|
for _, host := range b.AddBlockedHosts {
|
||||||
|
blockedHostsNode.Append(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.AddBlockedIPs) > 0 {
|
||||||
|
blockedIPsNode := node.Append("Blocked IP addresses:")
|
||||||
|
for _, ip := range b.AddBlockedIPs {
|
||||||
|
blockedIPsNode.Append(ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||||
|
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
|
||||||
|
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||||
|
blockedIPPrefixesNode.Append(ipNetwork.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
|
||||||
|
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||||
|
ipPrefixes []netip.Prefix, err error,
|
||||||
|
) {
|
||||||
|
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||||
|
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
|
||||||
|
}
|
||||||
170
internal/configuration/settings/dot.go
Normal file
170
internal/configuration/settings/dot.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"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.
|
||||||
|
// 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 over TLS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the DoT server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the DoT server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
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: gosettings.CopyPointer(d.Enabled),
|
||||||
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (d *DoT) overrideWith(other DoT) {
|
||||||
|
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) setDefaults() {
|
||||||
|
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||||
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
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.DoT.IPv4[0].Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
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", gosettings.BoolToYesNo(d.Enabled))
|
||||||
|
if !*d.Enabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
update := "disabled" //nolint:goconst
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||||
|
d.Enabled, err = reader.BoolPtr("DOT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||||
|
|
||||||
|
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
58
internal/configuration/settings/errors.go
Normal file
58
internal/configuration/settings/errors.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrValueUnknown = errors.New("value is unknown")
|
||||||
|
ErrCityNotValid = errors.New("the city specified is not valid")
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
|
||||||
|
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
||||||
|
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset 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")
|
||||||
|
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
||||||
|
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
||||||
|
ErrOpenVPNUserIsEmpty = errors.New("user is empty")
|
||||||
|
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
|
||||||
|
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
|
||||||
|
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||||
|
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
|
||||||
|
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
|
||||||
|
ErrRegionNotValid = errors.New("the region specified is not valid")
|
||||||
|
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||||
|
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||||
|
ErrSystemPUIDNotValid = errors.New("process user id 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")
|
||||||
|
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")
|
||||||
|
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
||||||
|
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
||||||
|
ErrWireguardEndpointPortSet = errors.New("endpoint port is 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")
|
||||||
|
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
|
||||||
|
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
|
||||||
|
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
|
||||||
|
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
|
||||||
|
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
|
||||||
|
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
|
||||||
|
)
|
||||||
142
internal/configuration/settings/firewall.go
Normal file
142
internal/configuration/settings/firewall.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Firewall contains settings to customize the firewall operation.
|
||||||
|
type Firewall struct {
|
||||||
|
VPNInputPorts []uint16
|
||||||
|
InputPorts []uint16
|
||||||
|
OutboundSubnets []netip.Prefix
|
||||||
|
Enabled *bool
|
||||||
|
Debug *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Firewall) validate() (err error) {
|
||||||
|
if hasZeroPort(f.VPNInputPorts) {
|
||||||
|
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasZeroPort(f.InputPorts) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasZeroPort(ports []uint16) (has bool) {
|
||||||
|
for _, port := range ports {
|
||||||
|
if port == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Firewall) copy() (copied Firewall) {
|
||||||
|
return Firewall{
|
||||||
|
VPNInputPorts: gosettings.CopySlice(f.VPNInputPorts),
|
||||||
|
InputPorts: gosettings.CopySlice(f.InputPorts),
|
||||||
|
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
|
||||||
|
Enabled: gosettings.CopyPointer(f.Enabled),
|
||||||
|
Debug: gosettings.CopyPointer(f.Debug),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (f *Firewall) overrideWith(other Firewall) {
|
||||||
|
f.VPNInputPorts = gosettings.OverrideWithSlice(f.VPNInputPorts, other.VPNInputPorts)
|
||||||
|
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
|
||||||
|
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
|
||||||
|
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
|
||||||
|
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Firewall) setDefaults() {
|
||||||
|
f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
|
||||||
|
f.Debug = gosettings.DefaultPointer(f.Debug, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Firewall) String() string {
|
||||||
|
return f.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("Firewall settings:")
|
||||||
|
|
||||||
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(f.Enabled))
|
||||||
|
if !*f.Enabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
if *f.Debug {
|
||||||
|
node.Appendf("Debug mode: on")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.VPNInputPorts) > 0 {
|
||||||
|
vpnInputPortsNode := node.Appendf("VPN input ports:")
|
||||||
|
for _, port := range f.VPNInputPorts {
|
||||||
|
vpnInputPortsNode.Appendf("%d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.InputPorts) > 0 {
|
||||||
|
inputPortsNode := node.Appendf("Input ports:")
|
||||||
|
for _, port := range f.InputPorts {
|
||||||
|
inputPortsNode.Appendf("%d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.OutboundSubnets) > 0 {
|
||||||
|
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||||
|
for _, subnet := range f.OutboundSubnets {
|
||||||
|
outboundSubnets.Appendf("%s", &subnet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
119
internal/configuration/settings/health.go
Normal file
119
internal/configuration/settings/health.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Health contains settings for the healthcheck and health server.
|
||||||
|
type Health struct {
|
||||||
|
// ServerAddress is the listening address
|
||||||
|
// for the health check server.
|
||||||
|
// It cannot be the empty string in the internal state.
|
||||||
|
ServerAddress string
|
||||||
|
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||||
|
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||||
|
ReadHeaderTimeout time.Duration
|
||||||
|
// ReadTimeout is the HTTP read timeout duration of the
|
||||||
|
// HTTP server. It defaults to 500 milliseconds.
|
||||||
|
ReadTimeout time.Duration
|
||||||
|
// TargetAddress is the address (host or host:port)
|
||||||
|
// to TCP dial to periodically for the health check.
|
||||||
|
// It cannot be the empty string in the internal state.
|
||||||
|
TargetAddress string
|
||||||
|
// SuccessWait is the duration to wait to re-run the
|
||||||
|
// healthcheck after a successful healthcheck.
|
||||||
|
// It defaults to 5 seconds and cannot be zero in
|
||||||
|
// the internal state.
|
||||||
|
SuccessWait time.Duration
|
||||||
|
// VPN has health settings specific to the VPN loop.
|
||||||
|
VPN HealthyWait
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Health) Validate() (err error) {
|
||||||
|
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.VPN.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("health VPN settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Health) copy() (copied Health) {
|
||||||
|
return Health{
|
||||||
|
ServerAddress: h.ServerAddress,
|
||||||
|
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||||
|
ReadTimeout: h.ReadTimeout,
|
||||||
|
TargetAddress: h.TargetAddress,
|
||||||
|
SuccessWait: h.SuccessWait,
|
||||||
|
VPN: h.VPN.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (h *Health) OverrideWith(other Health) {
|
||||||
|
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||||
|
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||||
|
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||||
|
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||||
|
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
||||||
|
h.VPN.overrideWith(other.VPN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Health) SetDefaults() {
|
||||||
|
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||||
|
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||||
|
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||||
|
const defaultReadTimeout = 500 * time.Millisecond
|
||||||
|
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||||
|
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||||
|
const defaultSuccessWait = 5 * time.Second
|
||||||
|
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
|
||||||
|
h.VPN.setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Health) String() string {
|
||||||
|
return h.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("Health settings:")
|
||||||
|
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||||
|
node.Appendf("Target address: %s", h.TargetAddress)
|
||||||
|
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
||||||
|
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||||
|
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||||
|
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||||
|
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.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.VPN.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("VPN health settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
internal/configuration/settings/healthywait.go
Normal file
76
internal/configuration/settings/healthywait.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||||
|
return HealthyWait{
|
||||||
|
Initial: gosettings.CopyPointer(h.Initial),
|
||||||
|
Addition: gosettings.CopyPointer(h.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 = gosettings.OverrideWithPointer(h.Initial, other.Initial)
|
||||||
|
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) setDefaults() {
|
||||||
|
const initialDurationDefault = 6 * time.Second
|
||||||
|
const additionDurationDefault = 5 * time.Second
|
||||||
|
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
|
||||||
|
h.Addition = gosettings.DefaultPointer(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) read(r *reader.Reader) (err error) {
|
||||||
|
h.Initial, err = r.DurationPtr(
|
||||||
|
"HEALTH_VPN_DURATION_INITIAL",
|
||||||
|
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Addition, err = r.DurationPtr(
|
||||||
|
"HEALTH_VPN_DURATION_ADDITION",
|
||||||
|
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
10
internal/configuration/settings/helpers/belong.go
Normal file
10
internal/configuration/settings/helpers/belong.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
|
||||||
|
for _, choice := range choices {
|
||||||
|
if value == choice {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
30
internal/configuration/settings/helpers_test.go
Normal file
30
internal/configuration/settings/helpers_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import gomock "github.com/golang/mock/gomock"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
182
internal/configuration/settings/httpproxy.go
Normal file
182
internal/configuration/settings/httpproxy.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPProxy contains settings to configure the HTTP proxy.
|
||||||
|
type HTTPProxy struct {
|
||||||
|
// User is the username to use for the HTTP proxy.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
User *string
|
||||||
|
// Password is the password to use for the HTTP proxy.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Password *string
|
||||||
|
// ListeningAddress is the listening address
|
||||||
|
// of the HTTP proxy server.
|
||||||
|
// It cannot be the empty string in the internal state.
|
||||||
|
ListeningAddress string
|
||||||
|
// Enabled is true if the HTTP proxy server should run,
|
||||||
|
// and false otherwise. It cannot be nil in the
|
||||||
|
// internal state.
|
||||||
|
Enabled *bool
|
||||||
|
// Stealth is true if the HTTP proxy server should hide
|
||||||
|
// each request has been proxied to the destination.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Stealth *bool
|
||||||
|
// Log is true if the HTTP proxy server should log
|
||||||
|
// each request/response. It cannot be nil in the
|
||||||
|
// internal state.
|
||||||
|
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) {
|
||||||
|
// Do not validate user and password
|
||||||
|
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPProxy) copy() (copied HTTPProxy) {
|
||||||
|
return HTTPProxy{
|
||||||
|
User: gosettings.CopyPointer(h.User),
|
||||||
|
Password: gosettings.CopyPointer(h.Password),
|
||||||
|
ListeningAddress: h.ListeningAddress,
|
||||||
|
Enabled: gosettings.CopyPointer(h.Enabled),
|
||||||
|
Stealth: gosettings.CopyPointer(h.Stealth),
|
||||||
|
Log: gosettings.CopyPointer(h.Log),
|
||||||
|
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||||
|
ReadTimeout: h.ReadTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
|
||||||
|
h.User = gosettings.OverrideWithPointer(h.User, other.User)
|
||||||
|
h.Password = gosettings.OverrideWithPointer(h.Password, other.Password)
|
||||||
|
h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress)
|
||||||
|
h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled)
|
||||||
|
h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth)
|
||||||
|
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() {
|
||||||
|
h.User = gosettings.DefaultPointer(h.User, "")
|
||||||
|
h.Password = gosettings.DefaultPointer(h.Password, "")
|
||||||
|
h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888")
|
||||||
|
h.Enabled = gosettings.DefaultPointer(h.Enabled, false)
|
||||||
|
h.Stealth = gosettings.DefaultPointer(h.Stealth, 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 {
|
||||||
|
return h.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("HTTP proxy settings:")
|
||||||
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(h.Enabled))
|
||||||
|
if !*h.Enabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Listening address: %s", h.ListeningAddress)
|
||||||
|
node.Appendf("User: %s", *h.User)
|
||||||
|
node.Appendf("Password: %s", gosettings.ObfuscateKey(*h.Password))
|
||||||
|
node.Appendf("Stealth mode: %s", gosettings.BoolToYesNo(h.Stealth))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
57
internal/configuration/settings/log.go
Normal file
57
internal/configuration/settings/log.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
"github.com/qdm12/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log contains settings to configure the logger.
|
||||||
|
type Log struct {
|
||||||
|
// Level is the log level of the logger.
|
||||||
|
// It cannot be empty in the internal state.
|
||||||
|
Level string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Log) validate() (err error) {
|
||||||
|
_, err = log.ParseLevel(l.Level)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("level: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) copy() (copied Log) {
|
||||||
|
return Log{
|
||||||
|
Level: l.Level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (l *Log) overrideWith(other Log) {
|
||||||
|
l.Level = gosettings.OverrideWithComparable(l.Level, other.Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) setDefaults() {
|
||||||
|
l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Log) String() string {
|
||||||
|
return l.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Log) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("Log settings:")
|
||||||
|
node.Appendf("Log level: %s", l.Level)
|
||||||
|
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
|
||||||
|
}
|
||||||
436
internal/configuration/settings/openvpn.go
Normal file
436
internal/configuration/settings/openvpn.go
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenVPN contains settings to configure the OpenVPN client.
|
||||||
|
type OpenVPN struct {
|
||||||
|
// Version is the OpenVPN version to run.
|
||||||
|
// It can only be "2.5" or "2.6".
|
||||||
|
Version string `json:"version"`
|
||||||
|
// User is the OpenVPN authentication username.
|
||||||
|
// It cannot be nil in the internal state if OpenVPN is used.
|
||||||
|
// It is usually required but in some cases can be the empty string
|
||||||
|
// to indicate no user+password authentication is needed.
|
||||||
|
User *string `json:"user"`
|
||||||
|
// Password is the OpenVPN authentication password.
|
||||||
|
// It cannot be nil in the internal state if OpenVPN is used.
|
||||||
|
// It is usually required but in some cases can be the empty string
|
||||||
|
// to indicate no user+password authentication is needed.
|
||||||
|
Password *string `json:"password"`
|
||||||
|
// ConfFile is a custom OpenVPN configuration file path.
|
||||||
|
// It can be set to the empty string for it to be ignored.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
ConfFile *string `json:"config_file_path"`
|
||||||
|
// Ciphers is a list of ciphers to use for OpenVPN,
|
||||||
|
// different from the ones specified by the VPN
|
||||||
|
// service provider configuration files.
|
||||||
|
Ciphers []string `json:"ciphers"`
|
||||||
|
// Auth is an auth algorithm to use in OpenVPN instead
|
||||||
|
// of the one specified by the VPN service provider.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
// It is ignored if it is set to the empty string.
|
||||||
|
Auth *string `json:"auth"`
|
||||||
|
// Cert is the base64 encoded DER of an OpenVPN certificate for the <cert> block.
|
||||||
|
// This is notably used by Cyberghost and VPN secure.
|
||||||
|
// It can be set to the empty string to be ignored.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Cert *string `json:"cert"`
|
||||||
|
// Key is the base64 encoded DER of an OpenVPN key.
|
||||||
|
// This is used by Cyberghost and VPN Unlimited.
|
||||||
|
// It can be set to the empty string to be ignored.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
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
|
||||||
|
// Private Internet Access. It can be set to an
|
||||||
|
// empty string for other providers.
|
||||||
|
PIAEncPreset *string `json:"pia_encryption_preset"`
|
||||||
|
// MSSFix is the value (1 to 10000) to set for the
|
||||||
|
// mssfix option for OpenVPN. It is ignored if set to 0.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
MSSFix *uint16 `json:"mssfix"`
|
||||||
|
// Interface is the OpenVPN device interface name.
|
||||||
|
// It cannot be an empty string in the internal state.
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
// ProcessUser is the OpenVPN process OS username
|
||||||
|
// to use. It cannot be empty in the internal state.
|
||||||
|
// It defaults to 'root'.
|
||||||
|
ProcessUser string `json:"process_user"`
|
||||||
|
// Verbosity is the OpenVPN verbosity level from 0 to 6.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Verbosity *int `json:"verbosity"`
|
||||||
|
// Flags is a slice of additional flags to be passed
|
||||||
|
// to the OpenVPN program.
|
||||||
|
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) {
|
||||||
|
// Validate version
|
||||||
|
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
|
||||||
|
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isCustom := vpnProvider == providers.Custom
|
||||||
|
isUserRequired := !isCustom &&
|
||||||
|
vpnProvider != providers.Airvpn &&
|
||||||
|
vpnProvider != providers.VPNSecure
|
||||||
|
|
||||||
|
if isUserRequired && *o.User == "" {
|
||||||
|
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordRequired := isUserRequired &&
|
||||||
|
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
|
||||||
|
|
||||||
|
if passwordRequired && *o.Password == "" {
|
||||||
|
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("custom configuration file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateOpenVPNClientKey(vpnProvider, *o.Key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypted key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
|
||||||
|
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxMSSFix = 10000
|
||||||
|
if *o.MSSFix > maxMSSFix {
|
||||||
|
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
||||||
|
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !regexpInterfaceName.MatchString(o.Interface) {
|
||||||
|
return fmt.Errorf("%w: '%s' does not match regex '%s'",
|
||||||
|
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.Verbosity < 0 || *o.Verbosity > 6 {
|
||||||
|
return fmt.Errorf("%w: %d can only be between 0 and 5",
|
||||||
|
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpenVPNConfigFilepath(isCustom bool,
|
||||||
|
confFile string,
|
||||||
|
) (err error) {
|
||||||
|
if !isCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpenVPNClientCertificate(vpnProvider,
|
||||||
|
clientCert string,
|
||||||
|
) (err error) {
|
||||||
|
switch vpnProvider {
|
||||||
|
case
|
||||||
|
providers.Airvpn,
|
||||||
|
providers.Cyberghost,
|
||||||
|
providers.VPNSecure,
|
||||||
|
providers.VPNUnlimited:
|
||||||
|
if clientCert == "" {
|
||||||
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientCert == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (o *OpenVPN) overrideWith(other OpenVPN) {
|
||||||
|
o.Version = gosettings.OverrideWithComparable(o.Version, other.Version)
|
||||||
|
o.User = gosettings.OverrideWithPointer(o.User, other.User)
|
||||||
|
o.Password = gosettings.OverrideWithPointer(o.Password, other.Password)
|
||||||
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
|
o.Ciphers = gosettings.OverrideWithSlice(o.Ciphers, other.Ciphers)
|
||||||
|
o.Auth = gosettings.OverrideWithPointer(o.Auth, other.Auth)
|
||||||
|
o.Cert = gosettings.OverrideWithPointer(o.Cert, other.Cert)
|
||||||
|
o.Key = gosettings.OverrideWithPointer(o.Key, other.Key)
|
||||||
|
o.EncryptedKey = gosettings.OverrideWithPointer(o.EncryptedKey, other.EncryptedKey)
|
||||||
|
o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase)
|
||||||
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
|
o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix)
|
||||||
|
o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface)
|
||||||
|
o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser)
|
||||||
|
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity)
|
||||||
|
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPN) setDefaults(vpnProvider string) {
|
||||||
|
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26)
|
||||||
|
o.User = gosettings.DefaultPointer(o.User, "")
|
||||||
|
if vpnProvider == providers.Mullvad {
|
||||||
|
o.Password = gosettings.DefaultPointer(o.Password, "m")
|
||||||
|
} else {
|
||||||
|
o.Password = gosettings.DefaultPointer(o.Password, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
|
o.Auth = gosettings.DefaultPointer(o.Auth, "")
|
||||||
|
o.Cert = gosettings.DefaultPointer(o.Cert, "")
|
||||||
|
o.Key = gosettings.DefaultPointer(o.Key, "")
|
||||||
|
o.EncryptedKey = gosettings.DefaultPointer(o.EncryptedKey, "")
|
||||||
|
o.KeyPassphrase = gosettings.DefaultPointer(o.KeyPassphrase, "")
|
||||||
|
|
||||||
|
var defaultEncPreset string
|
||||||
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
|
defaultEncPreset = presets.Strong
|
||||||
|
}
|
||||||
|
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
|
||||||
|
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
|
||||||
|
o.Interface = gosettings.DefaultComparable(o.Interface, "tun0")
|
||||||
|
o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root")
|
||||||
|
o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OpenVPN) String() string {
|
||||||
|
return o.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("OpenVPN settings:")
|
||||||
|
node.Appendf("OpenVPN version: %s", o.Version)
|
||||||
|
node.Appendf("User: %s", gosettings.ObfuscateKey(*o.User))
|
||||||
|
node.Appendf("Password: %s", gosettings.ObfuscateKey(*o.Password))
|
||||||
|
|
||||||
|
if *o.ConfFile != "" {
|
||||||
|
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Ciphers) > 0 {
|
||||||
|
node.Appendf("Ciphers: %s", o.Ciphers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.Auth != "" {
|
||||||
|
node.Appendf("Auth: %s", *o.Auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.Cert != "" {
|
||||||
|
node.Appendf("Client crt: %s", gosettings.ObfuscateKey(*o.Cert))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.Key != "" {
|
||||||
|
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 != "" {
|
||||||
|
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.MSSFix > 0 {
|
||||||
|
node.Appendf("MSS Fix: %d", *o.MSSFix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Interface != "" {
|
||||||
|
node.Appendf("Network interface: %s", o.Interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Run OpenVPN as: %s", o.ProcessUser)
|
||||||
|
|
||||||
|
node.Appendf("Verbosity level: %d", *o.Verbosity)
|
||||||
|
|
||||||
|
if len(o.Flags) > 0 {
|
||||||
|
node.Appendf("Flags: %s", o.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaults is a shorthand using setDefaults.
|
||||||
|
// It's used in unit tests in other packages.
|
||||||
|
func (o OpenVPN) WithDefaults(provider string) OpenVPN {
|
||||||
|
o.setDefaults(provider)
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
207
internal/configuration/settings/openvpnselection.go
Normal file
207
internal/configuration/settings/openvpnselection.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenVPNSelection struct {
|
||||||
|
// ConfFile is the custom configuration file path.
|
||||||
|
// It can be set to an empty string to indicate to
|
||||||
|
// NOT use a custom configuration file.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
ConfFile *string `json:"config_file_path"`
|
||||||
|
// Protocol is the OpenVPN network protocol to use,
|
||||||
|
// and can be udp or tcp. It cannot be the empty string
|
||||||
|
// in the internal state.
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
// CustomPort is the OpenVPN server endpoint port.
|
||||||
|
// It can be set to 0 to indicate no custom port should
|
||||||
|
// be used. It cannot be nil in the internal state.
|
||||||
|
CustomPort *uint16 `json:"custom_port"`
|
||||||
|
// PIAEncPreset is the encryption preset for
|
||||||
|
// Private Internet Access. It can be set to an
|
||||||
|
// empty string for other providers.
|
||||||
|
PIAEncPreset *string `json:"pia_encryption_preset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||||
|
// Validate ConfFile
|
||||||
|
if confFile := *o.ConfFile; confFile != "" {
|
||||||
|
err := validate.FileExists(confFile)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
||||||
|
providers.Giganews,
|
||||||
|
providers.Ipvanish,
|
||||||
|
providers.Perfectprivacy,
|
||||||
|
providers.Privado,
|
||||||
|
providers.Vyprvpn,
|
||||||
|
) {
|
||||||
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
|
ErrOpenVPNTCPNotSupported, vpnProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CustomPort
|
||||||
|
if *o.CustomPort != 0 {
|
||||||
|
switch vpnProvider {
|
||||||
|
// no restriction on port
|
||||||
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
|
providers.Privatevpn, providers.Torguard:
|
||||||
|
// no custom port allowed
|
||||||
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
|
providers.Privado, providers.Purevpn,
|
||||||
|
providers.Surfshark, providers.VPNSecure,
|
||||||
|
providers.VPNUnlimited, providers.Vyprvpn:
|
||||||
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
|
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
||||||
|
default:
|
||||||
|
var allowedTCP, allowedUDP []uint16
|
||||||
|
switch vpnProvider {
|
||||||
|
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}
|
||||||
|
allowedUDP = []uint16{53, 1194, 2049, 2050}
|
||||||
|
case providers.Mullvad:
|
||||||
|
allowedTCP = []uint16{80, 443, 1401}
|
||||||
|
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
|
||||||
|
case providers.Perfectprivacy:
|
||||||
|
allowedTCP = []uint16{44, 443, 4433}
|
||||||
|
allowedUDP = []uint16{44, 443, 4433}
|
||||||
|
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}
|
||||||
|
allowedUDP = []uint16{80, 1194, 1198}
|
||||||
|
case providers.Windscribe:
|
||||||
|
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||||
|
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedPorts := allowedUDP
|
||||||
|
if o.Protocol == constants.TCP {
|
||||||
|
allowedPorts = allowedTCP
|
||||||
|
}
|
||||||
|
err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: for VPN service provider %s: %w",
|
||||||
|
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate EncPreset
|
||||||
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
|
validEncryptionPresets := []string{
|
||||||
|
presets.None,
|
||||||
|
presets.Normal,
|
||||||
|
presets.Strong,
|
||||||
|
}
|
||||||
|
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
||||||
|
return OpenVPNSelection{
|
||||||
|
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||||
|
Protocol: o.Protocol,
|
||||||
|
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||||
|
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
||||||
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
|
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||||
|
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||||
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||||
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
|
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||||
|
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||||
|
|
||||||
|
var defaultEncPreset string
|
||||||
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
|
defaultEncPreset = presets.Strong
|
||||||
|
}
|
||||||
|
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OpenVPNSelection) String() string {
|
||||||
|
return o.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("OpenVPN server selection settings:")
|
||||||
|
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||||
|
|
||||||
|
if *o.CustomPort != 0 {
|
||||||
|
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.PIAEncPreset != "" {
|
||||||
|
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.ConfFile != "" {
|
||||||
|
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
215
internal/configuration/settings/portforward.go
Normal file
215
internal/configuration/settings/portforward.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PortForwarding contains settings for port forwarding.
|
||||||
|
type PortForwarding struct {
|
||||||
|
// Enabled is true if port forwarding should be activated.
|
||||||
|
// It cannot be nil for the internal state.
|
||||||
|
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
|
||||||
|
// to use. It can be the empty string to indicate not
|
||||||
|
// to write to a file. It cannot be nil for the
|
||||||
|
// internal state
|
||||||
|
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) {
|
||||||
|
if !*p.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate current provider or custom provider specified
|
||||||
|
providerSelected := vpnProvider
|
||||||
|
if *p.Provider != "" {
|
||||||
|
providerSelected = *p.Provider
|
||||||
|
}
|
||||||
|
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
|
||||||
|
if *p.Filepath != "" { // optional
|
||||||
|
_, err := filepath.Abs(*p.Filepath)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||||
|
return PortForwarding{
|
||||||
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
|
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) OverrideWith(other PortForwarding) {
|
||||||
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
|
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||||
|
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||||
|
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||||
|
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
|
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortForwarding) setDefaults() {
|
||||||
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
|
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 {
|
||||||
|
return p.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||||
|
if !*p.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
node = gotree.New("Automatic port forwarding settings:")
|
||||||
|
|
||||||
|
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
|
||||||
|
if filepath == "" {
|
||||||
|
filepath = "[not set]"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
19
internal/configuration/settings/portforward_test.go
Normal file
19
internal/configuration/settings/portforward_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_PortForwarding_String(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
settings := PortForwarding{
|
||||||
|
Enabled: ptrTo(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := settings.String()
|
||||||
|
|
||||||
|
assert.Empty(t, s)
|
||||||
|
}
|
||||||
128
internal/configuration/settings/provider.go
Normal file
128
internal/configuration/settings/provider.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider contains settings specific to a VPN provider.
|
||||||
|
type Provider struct {
|
||||||
|
// Name is the VPN service provider name.
|
||||||
|
// It cannot be the empty string in the internal state.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// ServerSelection is the settings to
|
||||||
|
// select the VPN server.
|
||||||
|
ServerSelection ServerSelection `json:"server_selection"`
|
||||||
|
// PortForwarding is the settings about port forwarding.
|
||||||
|
PortForwarding PortForwarding `json:"port_forwarding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
|
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||||
|
// Validate Name
|
||||||
|
var validNames []string
|
||||||
|
if vpnType == vpn.OpenVPN {
|
||||||
|
validNames = providers.AllWithCustom()
|
||||||
|
validNames = append(validNames, "pia") // Retro-compatibility
|
||||||
|
} else { // Wireguard
|
||||||
|
validNames = []string{
|
||||||
|
providers.Airvpn,
|
||||||
|
providers.Custom,
|
||||||
|
providers.Fastestvpn,
|
||||||
|
providers.Ivpn,
|
||||||
|
providers.Mullvad,
|
||||||
|
providers.Nordvpn,
|
||||||
|
providers.Protonvpn,
|
||||||
|
providers.Surfshark,
|
||||||
|
providers.Windscribe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
|
||||||
|
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.PortForwarding.Validate(p.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("port forwarding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) copy() (copied Provider) {
|
||||||
|
return Provider{
|
||||||
|
Name: p.Name,
|
||||||
|
ServerSelection: p.ServerSelection.copy(),
|
||||||
|
PortForwarding: p.PortForwarding.Copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) overrideWith(other Provider) {
|
||||||
|
p.Name = gosettings.OverrideWithComparable(p.Name, other.Name)
|
||||||
|
p.ServerSelection.overrideWith(other.ServerSelection)
|
||||||
|
p.PortForwarding.OverrideWith(other.PortForwarding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) setDefaults() {
|
||||||
|
p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess)
|
||||||
|
p.PortForwarding.setDefaults()
|
||||||
|
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Provider) String() string {
|
||||||
|
return p.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Provider) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("VPN provider settings:")
|
||||||
|
node.Appendf("Name: %s", p.Name)
|
||||||
|
node.AppendNode(p.ServerSelection.toLinesNode())
|
||||||
|
node.AppendNode(p.PortForwarding.toLinesNode())
|
||||||
|
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
|
||||||
|
}
|
||||||
175
internal/configuration/settings/publicip.go
Normal file
175
internal/configuration/settings/publicip.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublicIP contains settings for port forwarding.
|
||||||
|
type PublicIP struct {
|
||||||
|
// Enabled is set to true to fetch the public ip address
|
||||||
|
// information on VPN connection. It defaults to true.
|
||||||
|
Enabled *bool
|
||||||
|
// IPFilepath is the public IP address status file path
|
||||||
|
// to use. It can be the empty string to indicate not
|
||||||
|
// to write to a file. It cannot be nil for the
|
||||||
|
// internal state
|
||||||
|
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) {
|
||||||
|
if *p.IPFilepath != "" { // optional
|
||||||
|
_, err := filepath.Abs(*p.IPFilepath)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PublicIP) copy() (copied PublicIP) {
|
||||||
|
return PublicIP{
|
||||||
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
|
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
||||||
|
APIs: gosettings.CopySlice(p.APIs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||||
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
|
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
||||||
|
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PublicIP) setDefaults() {
|
||||||
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
|
||||||
|
p.IPFilepath = gosettings.DefaultPointer(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 {
|
||||||
|
return p.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||||
|
if !*p.Enabled {
|
||||||
|
return gotree.New("Public IP settings: disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
node = gotree.New("Public IP settings:")
|
||||||
|
|
||||||
|
if *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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
96
internal/configuration/settings/server.go
Normal file
96
internal/configuration/settings/server.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ControlServer contains settings to customize the control server operation.
|
||||||
|
type ControlServer struct {
|
||||||
|
// Address is the listening address to use.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Address *string
|
||||||
|
// Log can be true or false to enable logging on requests.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
const maxPrivilegedPort = 1023
|
||||||
|
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
|
||||||
|
return fmt.Errorf("%w: %d when running with user ID %d",
|
||||||
|
ErrControlServerPrivilegedPort, port, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControlServer) copy() (copied ControlServer) {
|
||||||
|
return ControlServer{
|
||||||
|
Address: gosettings.CopyPointer(c.Address),
|
||||||
|
Log: gosettings.CopyPointer(c.Log),
|
||||||
|
AuthFilePath: c.AuthFilePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (c *ControlServer) overrideWith(other ControlServer) {
|
||||||
|
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||||
|
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||||
|
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ControlServer) setDefaults() {
|
||||||
|
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||||
|
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||||
|
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ControlServer) String() string {
|
||||||
|
return c.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("Control server settings:")
|
||||||
|
node.Appendf("Listening address: %s", *c.Address)
|
||||||
|
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||||
|
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||||
|
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")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
549
internal/configuration/settings/serverselection.go
Normal file
549
internal/configuration/settings/serverselection.go
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
|
"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/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerSelection struct { //nolint:maligned
|
||||||
|
// VPN is the VPN type which can be 'openvpn'
|
||||||
|
// or 'wireguard'. It cannot be the empty string
|
||||||
|
// in the internal state.
|
||||||
|
VPN string `json:"vpn"`
|
||||||
|
// TargetIP is the server endpoint IP address to use.
|
||||||
|
// It will override any IP address from the picked
|
||||||
|
// built-in server. It cannot be the empty value in the internal
|
||||||
|
// state, and can be set to the unspecified address to indicate
|
||||||
|
// there is not target IP address to use.
|
||||||
|
TargetIP netip.Addr `json:"target_ip"`
|
||||||
|
// Countries is the list of countries to filter VPN servers with.
|
||||||
|
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 []string `json:"regions"`
|
||||||
|
// Cities is the list of cities to filter VPN servers with.
|
||||||
|
Cities []string `json:"cities"`
|
||||||
|
// ISPs is the list of ISP names to filter VPN servers with.
|
||||||
|
ISPs []string `json:"isps"`
|
||||||
|
// Names is the list of server names to filter VPN servers with.
|
||||||
|
Names []string `json:"names"`
|
||||||
|
// Numbers is the list of server numbers to filter VPN servers with.
|
||||||
|
Numbers []uint16 `json:"numbers"`
|
||||||
|
// Hostnames is the list of hostnames to filter VPN servers with.
|
||||||
|
Hostnames []string `json:"hostnames"`
|
||||||
|
// OwnedOnly is true if VPN provider servers that are not owned
|
||||||
|
// should be filtered. This is used with Mullvad.
|
||||||
|
OwnedOnly *bool `json:"owned_only"`
|
||||||
|
// FreeOnly is true if VPN servers that are not free should
|
||||||
|
// be filtered. This is used with ProtonVPN and VPN Unlimited.
|
||||||
|
FreeOnly *bool `json:"free_only"`
|
||||||
|
// PremiumOnly is true if VPN servers that are not premium should
|
||||||
|
// be filtered. This is used with VPN Secure.
|
||||||
|
// TODO extend to providers using FreeOnly.
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
// and the final connection.
|
||||||
|
OpenVPN OpenVPNSelection `json:"openvpn"`
|
||||||
|
// Wireguard contains settings to select Wireguard servers
|
||||||
|
// and the final connection.
|
||||||
|
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,
|
||||||
|
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||||
|
) (err error) {
|
||||||
|
switch ss.VPN {
|
||||||
|
case vpn.OpenVPN, vpn.Wireguard:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
|
||||||
|
if err != nil {
|
||||||
|
return err // already wrapped error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("OpenVPN server selection settings: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = ss.Wireguard.validate(vpnServiceProvider)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Wireguard server selection settings: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// Set an argument to nil to pass the check for a particular filter.
|
||||||
|
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
||||||
|
vpnServiceProvider string, warner Warner,
|
||||||
|
) (err error) {
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vpnServiceProvider == providers.Custom {
|
||||||
|
switch len(settings.Names) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
|
||||||
|
warner Warner,
|
||||||
|
) (err error) {
|
||||||
|
if len(values) > 0 && len(choices) == 0 {
|
||||||
|
return fmt.Errorf("%w", validate.ErrNoChoice)
|
||||||
|
}
|
||||||
|
|
||||||
|
set := make(map[string]struct{}, len(choices))
|
||||||
|
for _, choice := range choices {
|
||||||
|
lowercaseChoice := strings.ToLower(choice)
|
||||||
|
set[lowercaseChoice] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidValues := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
lowercaseValue := strings.ToLower(value)
|
||||||
|
_, 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 validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
|
||||||
|
switch {
|
||||||
|
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
|
||||||
|
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
|
||||||
|
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
|
||||||
|
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
|
||||||
|
case *settings.StreamOnly &&
|
||||||
|
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
||||||
|
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
|
||||||
|
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
|
||||||
|
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
|
||||||
|
case *settings.PortForwardOnly &&
|
||||||
|
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||||
|
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) {
|
||||||
|
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||||
|
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
||||||
|
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||||
|
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||||
|
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||||
|
ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities)
|
||||||
|
ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs)
|
||||||
|
ss.Hostnames = gosettings.OverrideWithSlice(ss.Hostnames, other.Hostnames)
|
||||||
|
ss.Names = gosettings.OverrideWithSlice(ss.Names, other.Names)
|
||||||
|
ss.Numbers = gosettings.OverrideWithSlice(ss.Numbers, other.Numbers)
|
||||||
|
ss.OwnedOnly = gosettings.OverrideWithPointer(ss.OwnedOnly, other.OwnedOnly)
|
||||||
|
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
|
||||||
|
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.Wireguard.overrideWith(other.Wireguard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||||
|
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||||
|
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
||||||
|
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||||
|
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||||
|
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, 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 := false
|
||||||
|
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
||||||
|
providers.PrivateInternetAccess, providers.Protonvpn) {
|
||||||
|
defaultPortForwardOnly = true
|
||||||
|
}
|
||||||
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
|
ss.Wireguard.setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss ServerSelection) String() string {
|
||||||
|
return ss.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("Server selection settings:")
|
||||||
|
node.Appendf("VPN type: %s", ss.VPN)
|
||||||
|
if !ss.TargetIP.IsUnspecified() {
|
||||||
|
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.Countries) > 0 {
|
||||||
|
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 {
|
||||||
|
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.Cities) > 0 {
|
||||||
|
node.Appendf("Cities: %s", strings.Join(ss.Cities, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.ISPs) > 0 {
|
||||||
|
node.Appendf("ISPs: %s", strings.Join(ss.ISPs, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.Names) > 0 {
|
||||||
|
node.Appendf("Server names: %s", strings.Join(ss.Names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.Numbers) > 0 {
|
||||||
|
numbersNode := node.Appendf("Server numbers:")
|
||||||
|
for _, number := range ss.Numbers {
|
||||||
|
numbersNode.Appendf("%d", number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss.Hostnames) > 0 {
|
||||||
|
node.Appendf("Hostnames: %s", strings.Join(ss.Hostnames, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.OwnedOnly {
|
||||||
|
node.Appendf("Owned only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.FreeOnly {
|
||||||
|
node.Appendf("Free only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.PremiumOnly {
|
||||||
|
node.Appendf("Premium only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.StreamOnly {
|
||||||
|
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 {
|
||||||
|
node.Appendf("Multi-hop only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ss.PortForwardOnly {
|
||||||
|
node.Appendf("Port forwarding only servers: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ss.VPN == vpn.OpenVPN {
|
||||||
|
node.AppendNode(ss.OpenVPN.toLinesNode())
|
||||||
|
} else {
|
||||||
|
node.AppendNode(ss.Wireguard.toLinesNode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaults is a shorthand using setDefaults.
|
||||||
|
// It's used in unit tests in other packages.
|
||||||
|
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
||||||
|
const portForwardingEnabled = false
|
||||||
|
ss.setDefaults(provider, portForwardingEnabled)
|
||||||
|
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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user