mirror of
https://github.com/qdm12/gluetun.git
synced 2026-06-27 14:27:29 +02:00
Compare commits
1138 Commits
v3.28.2
...
e409465cc5
| Author | SHA1 | Date | |
|---|---|---|---|
| e409465cc5 | |||
| a17591dcdb | |||
| c31c566282 | |||
| 496458ca4e | |||
| 52a41cb891 | |||
| 6c76273ef6 | |||
| 366062dc12 | |||
| 8abb05567c | |||
| a53a0267e4 | |||
| 4e986c8af7 | |||
| 6d84462f00 | |||
| acab89b91a | |||
| 48c1f2bf6a | |||
| c599e7fd2c | |||
| ff6e45fae0 | |||
| 17f24343d6 | |||
| ebbc630b31 | |||
| 39ac8b3432 | |||
| f65ee3dcb1 | |||
| 7e8d81b161 | |||
| 21e868c89c | |||
| 2e20e2df66 | |||
| 6f5f518d1d | |||
| 1998e0d04f | |||
| 14f30bc641 | |||
| f89e55b8ff | |||
| 7ad6af0947 | |||
| d3e089ccd7 | |||
| 3eebbf65a8 | |||
| a1ef736b0f | |||
| 46edfe49e3 | |||
| 7f9cd0fd0c | |||
| 1a25f7377a | |||
| 691dc3b0bf | |||
| 5fed7f70ed | |||
| eb9916f0ac | |||
| 2210a0e9ad | |||
| f8a677a424 | |||
| 8f012014d6 | |||
| b119325241 | |||
| 7720b1fad4 | |||
| 854bf5811d | |||
| 8f82376996 | |||
| cd19093d1d | |||
| fd12e5f9e7 | |||
| 3ca4b48887 | |||
| 38cf094573 | |||
| 5b01324d5f | |||
| 445f99d9dc | |||
| 891249849a | |||
| 5cae870745 | |||
| 4ea9884c6c | |||
| 3c84a94f11 | |||
| a9e8e427a5 | |||
| e1d31f53f1 | |||
| 548e4342c9 | |||
| 12f08bf5ad | |||
| 4ea2337668 | |||
| aab10f9d3f | |||
| 3e59447e58 | |||
| fed09562e5 | |||
| 4b819b4dbb | |||
| a0f6b208f7 | |||
| 44d510417e | |||
| 28ac8b859e | |||
| 3501782806 | |||
| 4106951340 | |||
| c47cc90181 | |||
| f8400c1b1c | |||
| 4bada8f0cd | |||
| b1b991b84e | |||
| 4a78989d9d | |||
| 9b6f048fe8 | |||
| 7a74d4f462 | |||
| 66b9f71ecf | |||
| 704a7fd7ef | |||
| f615e3c780 | |||
| f1a8303db7 | |||
| 628b0a22e2 | |||
| ea3d138bd6 | |||
| c3a6809447 | |||
| 792a5ff5f3 | |||
| 7eef1c89a7 | |||
| 8bc2fbd487 | |||
| a4eb625fbe | |||
| 17a7bf6d54 | |||
| b11de4f0c3 | |||
| e87a92efa0 | |||
| 44977f4d9e | |||
| c473579261 | |||
| d5eeec6fb3 | |||
| 7e7e8182ef | |||
| 64fd11d013 | |||
| 2006fae0e3 | |||
| 3b9c9b24bd | |||
| 11883aa830 | |||
| 1ae85aa5d0 | |||
| 763c5be119 | |||
| 5b88c76a14 | |||
| 086e3740f3 | |||
| 57cf276d31 | |||
| 405a6f699d | |||
| 72af17cc91 | |||
| 8a2e8bda0f | |||
| 5e6c11b045 | |||
| 85d2917e8e | |||
| 9a5995fa72 | |||
| 2438fc2c3a | |||
| 8aaf998fa1 | |||
| f0cbcbb60d | |||
| 4e5d4f7793 | |||
| 460ffb637a | |||
| c83d4b0926 | |||
| 00d1592899 | |||
| b5b0e01376 | |||
| b04529c380 | |||
| efea169495 | |||
| ba9fcb5b89 | |||
| 97ccadfd33 | |||
| e6fc792f4f | |||
| f4eeffe79a | |||
| 0394e31fe2 | |||
| e557971ae8 | |||
| a98b39a03f | |||
| 760fefd890 | |||
| 543d3fa95e | |||
| 93999062e4 | |||
| 853f4601a5 | |||
| 1d29f1f517 | |||
| d790e3385c | |||
| 069cde8a85 | |||
| d98afce793 | |||
| 57c53bc19e | |||
| c0af198155 | |||
| 3d53cea0f6 | |||
| f7a9ddc48b | |||
| 02a186c145 | |||
| 724cd3a15e | |||
| 199ad77ec9 | |||
| dd0edafbb1 | |||
| 9be2fc827b | |||
| b63702cf63 | |||
| ede2509132 | |||
| 100124e8b8 | |||
| 850a91b35f | |||
| 4a40f0fdee | |||
| b7735ecc00 | |||
| 457e5597bb | |||
| 2460b56c2b | |||
| 5b2f86f4e7 | |||
| 49317ecb8a | |||
| bd275aaea8 | |||
| 39bd9854f7 | |||
| c2c9504e94 | |||
| 48317a0d55 | |||
| 6c3f519c62 | |||
| b7cbea1ce6 | |||
| d8a3cc3dfa | |||
| b1da4c4b86 | |||
| 579bd8e416 | |||
| 7bf59ebfb4 | |||
| 4ac25b9dd1 | |||
| 4bcbd29fb9 | |||
| a8ee1d7a63 | |||
| c6c3a2bf1b | |||
| e7b25a0d5e | |||
| 11cd62f6b1 | |||
| ed26957a1a | |||
| 54b55c594f | |||
| ec24ffdfd8 | |||
| b9d49e0661 | |||
| 2bb4deccd5 | |||
| 0d0c0fb143 | |||
| 885e491bb7 | |||
| e75ae21dcd | |||
| 4b8dc8ded7 | |||
| 0eeee5c496 | |||
| d21953f62e | |||
| 034f8f6331 | |||
| 01487b5caf | |||
| 625a63e7c2 | |||
| 0c3e5d94d8 | |||
| d586793169 | |||
| c5eacac644 | |||
| 7fbf2cbee3 | |||
| 1dee183a70 | |||
| c66d8bed00 | |||
| 73b3e2c88a | |||
| ea87c0a2aa | |||
| 2192874de8 | |||
| 007c5159f4 | |||
| c6b211ef9b | |||
| 1c43a045d1 | |||
| 56b9e108be | |||
| 67b66bba9e | |||
| 8d86470905 | |||
| fb85ae79d1 | |||
| 783616f61d | |||
| bc79901f1e | |||
| 1c56189abc | |||
| 224618337c | |||
| 183d351b58 | |||
| 04d7cef294 | |||
| 5f903d1fbf | |||
| d43eb1658f | |||
| 36dfd5b631 | |||
| f81b8342d6 | |||
| cdec25da52 | |||
| 201d1041f4 | |||
| dc78b4ecce | |||
| d75b48d123 | |||
| e828ea1462 | |||
| be92aa2ac4 | |||
| 8f1fda7646 | |||
| 8eb990eb66 | |||
| 4698daea16 | |||
| b0a75673bd | |||
| 5f0c499808 | |||
| bdd69a1fb7 | |||
| 1af75bb30c | |||
| 9c1cd7e8b1 | |||
| facc6df3be | |||
| e292a4c9be | |||
| 9e4dd61c19 | |||
| fe3d4a94d4 | |||
| de38d759a4 | |||
| fba60af772 | |||
| 9b9b723887 | |||
| a10349e378 | |||
| 983330266a | |||
| 6eb511fb2a | |||
| 666f55767b | |||
| 0a0bb4cf71 | |||
| 2b0719225d | |||
| c97bd1bb7c | |||
| 10a7c75aa6 | |||
| 617f1b764f | |||
| 600f2ab643 | |||
| 7052d5490b | |||
| 6a5a836cb6 | |||
| a649b0adc1 | |||
| beaa8b5589 | |||
| e806fe02db | |||
| 92237658c3 | |||
| e627504fb5 | |||
| cc1c253bad | |||
| c27dac10fe | |||
| 7d1e2eb226 | |||
| 5b5aa5e014 | |||
| 9ee3ed754d | |||
| 0ca466fbd5 | |||
| 1c1d271967 | |||
| cc89b35b63 | |||
| fd6e5e4e90 | |||
| d702ed4122 | |||
| 2d00f3fe25 | |||
| 56db5a83c0 | |||
| f5206375c0 | |||
| c25c9f6f0e | |||
| 08a7aae5f1 | |||
| 57d8eb03c5 | |||
| 2b55161fbb | |||
| c4f2a224d4 | |||
| 8bb0cc324b | |||
| 2afa988174 | |||
| a35c994bc8 | |||
| 0fad44fb68 | |||
| 4f9dcff3f4 | |||
| 1abc90970d | |||
| a445ba072c | |||
| 9e5624d32b | |||
| 815fcdb711 | |||
| 0bb9f62755 | |||
| 93567a7804 | |||
| 0afbb71634 | |||
| 9f39d47150 | |||
| f9490656eb | |||
| 482421dda3 | |||
| 03f1fea123 | |||
| 31284542a2 | |||
| 5ff5fc4a5e | |||
| 5b93464fef | |||
| debf3474e7 | |||
| 2853ca9033 | |||
| 74d059dd77 | |||
| 9963e18a8a | |||
| 41cd8fb30d | |||
| 9ed6cd978d | |||
| c4b9d459ed | |||
| 6e99ca573e | |||
| 2cf4d6b469 | |||
| a17776673b | |||
| fcdba0a3cc | |||
| 4712d0cf79 | |||
| 113c113615 | |||
| 6023eb1878 | |||
| a1ece20617 | |||
| 0bc67b73a8 | |||
| c7ab5bd34c | |||
| 843bf08aa1 | |||
| 5b25cc95a9 | |||
| 0fddbc54a2 | |||
| 11fcfb7d19 | |||
| 3cd7d7edcb | |||
| 30609b6fe9 | |||
| 8a0921748b | |||
| 3fac02a82a | |||
| f11f142bee | |||
| 596faef8f2 | |||
| 3d1b6bc861 | |||
| 46ad576233 | |||
| 46beaac34b | |||
| 3025476e8b | |||
| cd6f9493a4 | |||
| 9984ad22d7 | |||
| 3565ba67c4 | |||
| ffb0bec4da | |||
| 4d2b8787e0 | |||
| d4831ad4a6 | |||
| 9e1b53a732 | |||
| d0113849d6 | |||
| 7b25fdfee8 | |||
| 5ed6e82922 | |||
| 7dbd14df27 | |||
| 96d8b53338 | |||
| 2bd19640d9 | |||
| 1047508bd7 | |||
| eb49306b80 | |||
| 43da9ddbb3 | |||
| 7fbc5c3c07 | |||
| e03f545e07 | |||
| 942f1f2c0f | |||
| baf566d7a5 | |||
| 6712adfe6b | |||
| 2e2e5f9df5 | |||
| 35e9b2365d | |||
| b0b769d2c1 | |||
| d3c7d3c7bc | |||
| 65f49ea012 | |||
| 5687555921 | |||
| 0fb75036a0 | |||
| 2b513dd43d | |||
| 687d9b4736 | |||
| c70c2ef932 | |||
| af3ada109b | |||
| 9d40564734 | |||
| 3734815ada | |||
| b9cc5c1fdc | |||
| c646ca5766 | |||
| 1394be5143 | |||
| 93442526f8 | |||
| d85402050b | |||
| b1c62cb525 | |||
| fae64a297a | |||
| 6e2682a9ce | |||
| 555049f09c | |||
| 712f7c3d35 | |||
| 7a51c211cd | |||
| c48189c1c4 | |||
| 9803fa1cfd | |||
| cf756f561a | |||
| a4021fedc3 | |||
| 31a36a9250 | |||
| 36fe349b70 | |||
| 3ef1cfd97c | |||
| 669feb45f1 | |||
| 85890520ab | |||
| 340016521e | |||
| ef523df42c | |||
| 5306e3bab1 | |||
| 72a49afd2b | |||
| 9b8edbb81e | |||
| a1554feb3f | |||
| 490410bf09 | |||
| 8c113f5268 | |||
| 075cbd5a0f | |||
| d82df2b431 | |||
| a09f8214d9 | |||
| 396e9c003e | |||
| b0c4a28be6 | |||
| 85325e4a31 | |||
| 9933dd3ec5 | |||
| 13532c8b4b | |||
| 3926797295 | |||
| febd3f784f | |||
| 61b053f0e1 | |||
| 8dae352ccc | |||
| e890c50da6 | |||
| ddd9f4d021 | |||
| 7e58b4baee | |||
| a21fbb9a4f | |||
| 3b7d27c919 | |||
| 68ddbfc0fe | |||
| a2047cb800 | |||
| fdd499146c | |||
| 37900341cf | |||
| 36bb368cad | |||
| f9bdb219d0 | |||
| 0374c14e42 | |||
| a035a151bd | |||
| e69966381d | |||
| 94dfb2b1f2 | |||
| 92011205be | |||
| c9707646bd | |||
| c50705736b | |||
| ec284c17f4 | |||
| ad6c52dc4c | |||
| 5f182febae | |||
| 86d82c1098 | |||
| 842b9004da | |||
| 6ac7ca4f0f | |||
| ddfcbe1bee | |||
| 88fd9388e4 | |||
| 69aafa53c9 | |||
| 3473fe9c15 | |||
| c655500045 | |||
| 96a8015af6 | |||
| ddd3876f92 | |||
| f1f34722ee | |||
| 937c667ca8 | |||
| 3c45f57aaa | |||
| 30640eefe2 | |||
| 8567522594 | |||
| bd8214e648 | |||
| a61302f135 | |||
| 3dfb43e117 | |||
| 2388e0550b | |||
| a7d70dd9a3 | |||
| 76a4bb5dc3 | |||
| 3daf15a612 | |||
| 81ffbaf057 | |||
| abe9dcbe33 | |||
| 3c8e80a1a4 | |||
| 694988b32f | |||
| ea31886299 | |||
| 5b2923ca65 | |||
| 432eaa6c04 | |||
| 5fd0af9395 | |||
| 03deb9aed0 | |||
| cbdd1a933c | |||
| 99e9bc87cf | |||
| 9ef14ee070 | |||
| 7842ff4cdc | |||
| 3d6d03b327 | |||
| 7ebbaf4351 | |||
| c665b13cec | |||
| 970b21a6eb | |||
| 62747f1eb8 | |||
| a2e76e1683 | |||
| 07651683f9 | |||
| 429aea8e0f | |||
| 01fa9934bc | |||
| ff7cadb43b | |||
| 540acc915d | |||
| 703a546c1d | |||
| 4851bd70da | |||
| a2b3d7e30c | |||
| 4d60b71583 | |||
| 3f130931d2 | |||
| 946f055fed | |||
| eaece0cb8e | |||
| 4203f4fabf | |||
| c39edb6378 | |||
| b3cc2781ff | |||
| 12c411e203 | |||
| 3bf937d705 | |||
| bc55c25e73 | |||
| 897a9d7f57 | |||
| 4a128677dd | |||
| 9233f3f5ba | |||
| 11c2354408 | |||
| 1f2882434a | |||
| 01aaf2c86a | |||
| d260ac7a49 | |||
| 0bea0d4ecd | |||
| 59994bd6e7 | |||
| 62799d2449 | |||
| 09c47c740c | |||
| ecbfc02713 | |||
| 7be9288685 | |||
| d1f57d0e36 | |||
| 74ea1a0f5a | |||
| 2a9ab29e7d | |||
| 8be78a5741 | |||
| 4a669c3458 | |||
| ae5b71a864 | |||
| 6fff2ce1a4 | |||
| f6165d206a | |||
| 8dbe7b8888 | |||
| 10f43d7a70 | |||
| 01283def17 | |||
| b32e085354 | |||
| ac9446e296 | |||
| dea4080a7b | |||
| 2e63dba817 | |||
| 10384c9e37 | |||
| 34e8f5f3a9 | |||
| ceb6ff4ca4 | |||
| 4c3da54303 | |||
| 5d75bbc869 | |||
| 72e227f87d | |||
| c5c37e7f96 | |||
| aaf3019d8c | |||
| 5191f3558f | |||
| 13ffffb157 | |||
| 7bc2972b27 | |||
| ab08a5e666 | |||
| 8c730a6e4a | |||
| 4c47b6f142 | |||
| 264480b659 | |||
| cb99f90bb5 | |||
| 2bf2525bc5 | |||
| 26705f5a23 | |||
| ddbfdc9f14 | |||
| 9807d5f8f5 | |||
| 921992ebc7 | |||
| 8331ce6010 | |||
| 36c8da7ea7 | |||
| 73832d8b49 | |||
| a03041cfea | |||
| e7381b3800 | |||
| 9d50c23532 | |||
| 0501743814 | |||
| 06c9bc55d3 | |||
| fe05521f2b | |||
| 93ed87d12b | |||
| 4218dba177 | |||
| 7872ab91dc | |||
| c9e75bd697 | |||
| 7453f7f59a | |||
| 19a9ac9fd7 | |||
| ecb06836b5 | |||
| 1e25372189 | |||
| 6042a9e3c2 | |||
| fd4689ee70 | |||
| 4bd16373f2 | |||
| ce642a6d8b | |||
| ef6874fe57 | |||
| 29bc60bc35 | |||
| fb145d68a0 | |||
| 6dd27e53d4 | |||
| e0a977cf83 | |||
| 4d002a3ad6 | |||
| 4206859cad | |||
| 5dacbb994f | |||
| ebf4bf9ea8 | |||
| 241a9930c9 | |||
| f1e8200cfc | |||
| 03eddb1698 | |||
| b25ee21e3e | |||
| 7e0738d113 | |||
| 0b078e5f5e | |||
| 45fe38e670 | |||
| 72e2e4b82c | |||
| bdc594c297 | |||
| 1afe01d8cd | |||
| 234e54ac5c | |||
| 49b8f8b443 | |||
| ce75c5ca21 | |||
| e07966f71e | |||
| c5395adfea | |||
| 9d1ec69b73 | |||
| ee8802ee86 | |||
| 0d7115c832 | |||
| 08fb049f63 | |||
| c87c0e12fe | |||
| 7b4befce61 | |||
| 6709a248d6 | |||
| bf4cc0dabf | |||
| 982100782c | |||
| 4afbe9332f | |||
| 4019ee3ea1 | |||
| e859c60343 | |||
| 8454123cae | |||
| 6b2f350ec9 | |||
| e01ce9c6d8 | |||
| ecc80a5a9e | |||
| 23b0320cfb | |||
| 3e79509c97 | |||
| 2185f347ce | |||
| aa3ef5a1c2 | |||
| acec050b95 | |||
| 9ca97fb04f | |||
| 4776948af6 | |||
| 4d9c619b24 | |||
| 62007bf1a1 | |||
| 7674efe8d7 | |||
| b3ceece779 | |||
| c74e4178bb | |||
| c0621bf381 | |||
| fb00fb16c2 | |||
| 6096b7ad4b | |||
| 9cb4c74493 | |||
| e470dc8a12 | |||
| ab49f1f733 | |||
| 62158a1739 | |||
| 3d16798544 | |||
| b51aa0c6b9 | |||
| 84d00b42f1 | |||
| e201856667 | |||
| 3254fc8aa6 | |||
| 4bca4ca932 | |||
| a20695ffb3 | |||
| d01cfef039 | |||
| 0eed558b10 | |||
| 423a5c37e0 | |||
| cfca026621 | |||
| 6a6337b98f | |||
| 72b5afc771 | |||
| 659bc0c9cb | |||
| 827e591174 | |||
| a369745101 | |||
| 586b0e17a0 | |||
| b5f1055682 | |||
| 6b9c775055 | |||
| d8b9b2a85b | |||
| c826707d42 | |||
| 8a17cd87c3 | |||
| f8da1e79bc | |||
| cfc29d6a6b | |||
| 5467652b8b | |||
| daa63c276d | |||
| ab96acdc5b | |||
| 6e108706a1 | |||
| 4a6c229504 | |||
| ed3a72790a | |||
| 4bf5777f23 | |||
| f0f9bdb883 | |||
| 4984d90b5a | |||
| b5e648d13a | |||
| f71a1b083b | |||
| 75fd869625 | |||
| 657b4b787f | |||
| 32d6453918 | |||
| c326b616b4 | |||
| d5376629df | |||
| 3e825d7a08 | |||
| 059b12883f | |||
| 74aa509644 | |||
| 4105f74ce1 | |||
| 8318be3159 | |||
| de196490db | |||
| ab7d1ccf3d | |||
| ed49a7a7c0 | |||
| 135832d985 | |||
| 1adbd9f692 | |||
| 26e1c92841 | |||
| 3c5b3514fb | |||
| f884293f6e | |||
| c67bd1aa2a | |||
| 77ace9377d | |||
| 6e676209ff | |||
| 80917d58b2 | |||
| fa49f13f19 | |||
| 1fcabd152f | |||
| 385879c297 | |||
| e0515cb458 | |||
| 1c43a1d55b | |||
| 6c639fcf7f | |||
| ec1f252528 | |||
| ee413f59a2 | |||
| d4df87286e | |||
| a194906bdd | |||
| 9b00763a69 | |||
| 4d627bb7b1 | |||
| dc8fc5f81f | |||
| b787e12e25 | |||
| f96448947f | |||
| e64e5af4c3 | |||
| aa6dc786a4 | |||
| 84300db7c1 | |||
| 2ac0f35060 | |||
| 1a865f56d5 | |||
| 0406de399d | |||
| 71201411f4 | |||
| c435bbb32c | |||
| 4cbfea41f2 | |||
| f9c9ad34f7 | |||
| 4ea474b896 | |||
| 6aa4a93665 | |||
| ea25a0ff89 | |||
| 659da67ed5 | |||
| ffc6d2e593 | |||
| 03ce08e23d | |||
| 3449e7a0e1 | |||
| c0062fb807 | |||
| 1ac031e78c | |||
| e556871e8b | |||
| 082a38b769 | |||
| 39ae57f49d | |||
| 9024912e17 | |||
| eecfb3952f | |||
| 0ebfe534d3 | |||
| c5cc240a6c | |||
| 1a5a0148ea | |||
| abe2aceb18 | |||
| fa541b8fc2 | |||
| a681d38dfb | |||
| a7b96e3f4d | |||
| 04ef92edab | |||
| 919b55c3aa | |||
| 9c0f187a12 | |||
| 075a1e2a80 | |||
| f31a846cda | |||
| 9bef46db77 | |||
| d83217f7ac | |||
| 1cd2fec796 | |||
| 235f24ee5b | |||
| 2e34c6009e | |||
| c0eb2f2315 | |||
| 8ad16cdc12 | |||
| fae6544431 | |||
| f8a41b2133 | |||
| ff9b56d6d8 | |||
| 99d5a591b9 | |||
| fbe252a9b6 | |||
| 76a92b90e3 | |||
| 2873b06275 | |||
| 9cdd6294d2 | |||
| 44bc60b00d | |||
| 6f0be57860 | |||
| d3d8484b8e | |||
| 515ae8efb3 | |||
| 83826e1253 | |||
| 4292a500ae | |||
| 4a0f9c36ba | |||
| ea1991496e | |||
| 4675572328 | |||
| 412921fc1f | |||
| 1c905d0e6f | |||
| 2ec9293324 | |||
| 9b39a301a8 | |||
| cade2b99bf | |||
| 40cdb4f662 | |||
| c58d6d4de2 | |||
| 0da2b6ad0b | |||
| 37f0e5c73b | |||
| a9cd7be3f9 | |||
| 07459ee854 | |||
| 943943e8d1 | |||
| 5927ee9dec | |||
| 3b136e02db | |||
| 482447c151 | |||
| 5d8fbf8006 | |||
| 2ab80771d9 | |||
| 7399c00508 | |||
| 2d2f657851 | |||
| 0e21fdc9de | |||
| b87b2109b1 | |||
| 2c30984a10 | |||
| 47593928f9 | |||
| b961284845 | |||
| b5d230d47a | |||
| c2972f7bf6 | |||
| aed235f52d | |||
| bfe5e4380f | |||
| eca182a32f | |||
| caabaf918e | |||
| d6924597dd | |||
| c26476a2fd | |||
| 5be0d0bbba | |||
| 38ddcfa756 | |||
| 163ac48ce4 | |||
| def407d610 | |||
| 22b2e2cc6e | |||
| c92962e97c | |||
| 9d1a0b60a2 | |||
| 9cf2c9c4d2 | |||
| e7150ba254 | |||
| 7ba70f19ef | |||
| 9488a9f88a | |||
| 020196f1c3 | |||
| 7e325715c7 | |||
| 75670a80b8 | |||
| a43973c093 | |||
| 1827a03afd | |||
| 3100cc1e5e | |||
| eed62fdc6d | |||
| d2b8dbcb10 | |||
| 90d43856ef | |||
| 86f95cb390 | |||
| 3b807e2ca9 | |||
| e8f2296a0d | |||
| 1dd38bc658 | |||
| 63303bc311 | |||
| 5200ee5722 | |||
| 86ec75722a | |||
| 0a29337c3b | |||
| 00ee6ff9a7 | |||
| 6d0a2a968f | |||
| 4bb77ebcc5 | |||
| 56ecfcb9f4 | |||
| 9a0fcbc011 | |||
| b6c8399c3b | |||
| 7a88a09341 | |||
| 912b31cfc6 | |||
| d21a943779 | |||
| 801a7fd6fe | |||
| 80053f6b7d | |||
| e165bb6870 | |||
| 67bd1171ae | |||
| 4e2e46014d | |||
| 1693c59e0d | |||
| 9d4105ee59 | |||
| 19585da3bc | |||
| 51f830cfc1 | |||
| 804ea7ebd6 | |||
| 3294b8df60 | |||
| d77ec7a6cb | |||
| 219d1f371c | |||
| fa7fd5f076 | |||
| d4f8eea7bf | |||
| 723d0f5e12 | |||
| 20f4d8cc0b | |||
| 64cca69bf3 | |||
| fc8a2abb8f | |||
| 16ecf48b89 | |||
| 8fa4fd1b64 | |||
| 4db6d1ecf9 | |||
| 3b86927ca7 | |||
| 8bfa2f9b27 | |||
| fe2a3e4d11 | |||
| b0451d8e50 | |||
| a0b9044fd3 | |||
| c7a841f4b4 | |||
| 4ba159e483 | |||
| 63a696d7e7 | |||
| d457342b46 | |||
| c246dae2cc | |||
| 0f4a2e5224 | |||
| db262050d5 | |||
| 227cdea0c8 | |||
| 33a6f1c01b | |||
| f6f3c110f0 | |||
| 27a3f2c846 | |||
| 62169baeea | |||
| 4b18636a91 | |||
| 51432ca05f | |||
| b5ebdcd040 | |||
| 416c1ee113 | |||
| fe97e28461 | |||
| cbd8711a21 | |||
| 7578e52ed5 | |||
| 0df68f76d5 | |||
| 9a528c42f8 | |||
| 5607916af6 | |||
| 4ad7a2a444 | |||
| ab5dbdca97 | |||
| a97fcda283 | |||
| e955adc1e1 | |||
| ac5141b411 | |||
| f8c189e48a | |||
| 2f2a904c64 | |||
| 9261dca8ab | |||
| 7b5d5c3884 | |||
| 7c80d80904 | |||
| ea40b84ec0 | |||
| 4e6ef649c4 | |||
| dd40f1d2e6 | |||
| 490693bb26 | |||
| c8d33ca5f3 | |||
| e6df026332 | |||
| 7a30343053 | |||
| fc02ae9c13 | |||
| f70f0aca9c | |||
| 16acd1b162 | |||
| 2e3eb1fd7b | |||
| a4cf17f81e | |||
| c0a301611d | |||
| cc934f5c68 | |||
| 74426f6202 | |||
| 03ed3cb1c8 | |||
| 1b1335835b | |||
| 5070dbcf7f | |||
| 90b9d85742 | |||
| 7a3b9941aa | |||
| 698095f0a0 | |||
| 5a06d8e155 | |||
| 7421dcb45f | |||
| 554a6cdb92 | |||
| 5aa39be973 | |||
| 192a7a56a3 | |||
| 1d1657e9be | |||
| 49b7301295 | |||
| 126804c15e | |||
| a7643c6201 | |||
| db2de5fc84 | |||
| 5c7b9aa6a1 | |||
| 63890c159e | |||
| e7d5ae5dc1 | |||
| b275354a92 | |||
| ac02a64d17 | |||
| 9c80150e09 | |||
| 31a8bc9062 | |||
| f15dde6502 | |||
| f70609c464 | |||
| c954e6f231 | |||
| cb804577a9 | |||
| e5be20d719 | |||
| 875690ab18 | |||
| 6a5aa8eddb | |||
| 7fdc7de210 | |||
| dd7630997b | |||
| aba5ca4536 | |||
| 7506625f40 | |||
| 5ddd703f6a | |||
| 71c51a7455 | |||
| 284d4340b1 | |||
| 2c1281d0a2 | |||
| 532df9f8d4 | |||
| 45b7da1058 | |||
| 907daff483 | |||
| 7757e8a114 | |||
| e59e28152f | |||
| 2fe0594db7 | |||
| 794e96b449 | |||
| 07282f414c | |||
| e583f9de47 | |||
| 8570e09eb9 | |||
| ae5cba519c | |||
| 26f3832187 | |||
| 5989f29035 | |||
| 4ace99f318 | |||
| d1c5e00df8 | |||
| 5eacb46226 | |||
| 6c17612310 | |||
| fba73a0a0f | |||
| 4faef87c03 | |||
| 5914cb0e37 | |||
| aa53436e56 | |||
| 8dfaebc737 | |||
| 062b6a276c | |||
| 647cd07de7 | |||
| a530c84c5f | |||
| 0bb320065e | |||
| d685d78e74 | |||
| 48896176e5 | |||
| 54dcf28b31 | |||
| f8bf32bb34 | |||
| 748923021c | |||
| a182e3503b | |||
| 991cfb8659 | |||
| d0dfc21e2b | |||
| 617bd0c600 | |||
| 349b5429ba | |||
| 8db2944749 | |||
| 5986432a22 | |||
| 652daec509 | |||
| f94d4b761a | |||
| 1ab74e6bb3 | |||
| 8e101d49a1 | |||
| 7c08e8f607 | |||
| a4caa61c47 | |||
| ebae167815 | |||
| a6f00f2fb2 | |||
| 877617cc53 | |||
| 2800588ef7 | |||
| f5efa42aaf | |||
| 10bd0e1505 | |||
| a4c80b3045 | |||
| dbb71bd695 | |||
| a544f6e604 | |||
| a18e026b70 | |||
| 0413a0a1ab | |||
| cb6e9cb761 | |||
| 420ae40901 | |||
| 34e67f9f99 | |||
| 18c53aa597 | |||
| 6d2f9b9508 | |||
| 6826b05d58 | |||
| 9f959dbc6a | |||
| 87dbae5745 | |||
| 037f19e852 | |||
| 62ad8bcd8f | |||
| 2805c3388a | |||
| 535297dcf5 | |||
| b3b6933ef4 | |||
| edbbcc041a | |||
| d430ebc34f | |||
| 0e9abc6e1d | |||
| 0c0dd10766 | |||
| 75454be6b6 | |||
| 4952e3b74e | |||
| 04b34a266c | |||
| 89b6a031b0 | |||
| d4c6a9bdb5 | |||
| cdc29d48b7 | |||
| f4b464a7cf | |||
| 76690d3add | |||
| 9898387579 | |||
| 1ea15a1a13 | |||
| bda6707685 | |||
| 89277828ac | |||
| 83b4a3fe55 | |||
| 45c9e780c0 | |||
| 33b8f5f596 | |||
| 447a7c9891 | |||
| 1bd355ab96 | |||
| 578ef768ab | |||
| 0378fe4a7b | |||
| ebd94723c1 | |||
| 11b55abff3 | |||
| 7f32b43895 | |||
| 899f10c35e | |||
| 415cb7a945 | |||
| e37f557cd5 | |||
| 79f213d97a | |||
| 11e1c9f9bb | |||
| 3ff3816d77 | |||
| c0bdae8baf | |||
| 46e6bd16c9 | |||
| 5359257c65 | |||
| 5e659dc5b3 | |||
| 85e9d7d522 | |||
| b71c8e58f4 | |||
| e998372ce2 | |||
| 1216326867 | |||
| f53f0cfffd | |||
| f5f65d534a | |||
| 684cef6eab | |||
| b4f6ae030d | |||
| e95c94294f | |||
| 36b504609b | |||
| 1e6b4ed5eb | |||
| 0549326dfb | |||
| 87c6ebe1c5 | |||
| f0afac243b | |||
| 53472077f4 | |||
| 55afdf33e1 | |||
| d3c1f9263c | |||
| 6341d1dda6 | |||
| e62e1883c2 | |||
| 501b98dbd3 | |||
| 029fd1da1f | |||
| fd0267efef | |||
| 4414366370 | |||
| 08553bc90b | |||
| 6f850c4ad4 | |||
| 8e1316bd8a | |||
| b345368257 | |||
| 90dd3b1b5c | |||
| 22455ac76f | |||
| eb18eaf0a9 | |||
| 90c6c8485b | |||
| 381089ebdf | |||
| 292813831d | |||
| 991d75a1d0 | |||
| d9dfb81cb4 | |||
| 67a9cacb61 | |||
| a91eb95456 | |||
| a295269518 | |||
| 42904b6749 | |||
| 364f9de756 | |||
| 7fd45cf17f | |||
| eb71cfb144 | |||
| 48e469917e | |||
| 4bcd8ee9f5 | |||
| 1b2bcf901a | |||
| 306de8feda | |||
| e3696f1eea | |||
| 7ff14a356c | |||
| 4bde50fb3a | |||
| bd0868d764 | |||
| 5ffe8555ba | |||
| 78ccbb21cd | |||
| 92dbe1ebad | |||
| 2eec60cdd2 | |||
| da8c104ebd | |||
| 0ef7b66047 | |||
| e32d251cc1 | |||
| 9dd5e7bf1d | |||
| b6de6035f6 | |||
| 88ccaf0b83 | |||
| 52c8bc075f | |||
| 2537cd5271 | |||
| db91625de4 | |||
| df78386fbe | |||
| a1d70f740a | |||
| 187f42277a | |||
| e1f89bb569 | |||
| 1d94f8ab2b | |||
| 045ecabb78 | |||
| e6c3cb078a | |||
| afa51b3ff6 | |||
| f9c80b2285 | |||
| fc5cf44b2c | |||
| 0c0f1663b1 | |||
| 306d8494d6 | |||
| f5c00c3e2d | |||
| ac9571c6b2 | |||
| 934fafb64b | |||
| d51514015f | |||
| a9cfd16d53 | |||
| 1a6f26fa3b | |||
| 0dd723b29f | |||
| 7ad6fc8e73 | |||
| 31c7e6362b | |||
| 072b42d867 | |||
| 5d66c193aa | |||
| aa729515b9 | |||
| 54b7e23974 | |||
| ad80e0c1ab | |||
| 5d7b278957 | |||
| 678caaf6a0 | |||
| 7228cd7b12 | |||
| 7b598a3534 | |||
| 9cdc9e9153 | |||
| 71ab0416b0 | |||
| 10a13bc8a7 | |||
| be386a8e33 | |||
| c33fb8bb97 | |||
| 20f20f051b | |||
| 179274ade0 | |||
| 84607e332b | |||
| 8186ef2342 | |||
| 19b184adba | |||
| a97fd35d6e | |||
| 470ca020e2 | |||
| f64d7c4343 | |||
| c6f68a64e6 | |||
| 5aaa122460 | |||
| de169c027f | |||
| 314c9663a2 | |||
| 21995eb3e3 | |||
| 6fc700bd62 | |||
| acdbe2163e | |||
| c3a231e0ab | |||
| 984e143336 | |||
| e2ba2f82c0 | |||
| ace5e97e68 | |||
| 82d42297e8 | |||
| f99d5e8656 | |||
| 0795008c23 | |||
| c975a86a70 | |||
| 69eee345d2 | |||
| 48afc05bcb | |||
| 39a62f5db7 | |||
| 006b218ade |
@@ -1,5 +1,4 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add wireguard-tools
|
||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||
RUN apk add wireguard-tools htop openssl tcpdump iptables
|
||||
RUN apk add nodejs npm && npm install markdownlint-cli2 --global && apk del npm
|
||||
|
||||
+26
-47
@@ -2,68 +2,47 @@
|
||||
|
||||
Development container that can be used with VSCode.
|
||||
|
||||
It works on Linux, Windows and OSX.
|
||||
It works on Linux, Windows (WSL2) and OSX.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
- Ensure your host has the following and that they are accessible by Docker:
|
||||
- `~/.ssh` directory
|
||||
- `~/.gitconfig` file (can be empty)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the following files and directory on your host if you don't have them:
|
||||
|
||||
```sh
|
||||
touch ~/.gitconfig ~/.zsh_history
|
||||
mkdir -p ~/.ssh
|
||||
```
|
||||
|
||||
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
|
||||
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
### Customize the image
|
||||
For any customization to take effect, you should "rebuild and reopen":
|
||||
|
||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||
2. Select `Dev-Containers: Rebuild Container`
|
||||
|
||||
```Dockerfile
|
||||
FROM qmcgaw/godevcontainer
|
||||
USER root
|
||||
RUN apk add curl
|
||||
USER vscode
|
||||
```
|
||||
Changes you can make are notably:
|
||||
|
||||
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
|
||||
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
||||
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
||||
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
||||
|
||||
To rebuild the image, either:
|
||||
|
||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
||||
- With a terminal, go to this directory and `docker-compose build`
|
||||
|
||||
### Customize VS code settings
|
||||
|
||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
|
||||
|
||||
### Publish a port
|
||||
|
||||
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
|
||||
|
||||
### Run other services
|
||||
|
||||
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
|
||||
|
||||
```yml
|
||||
database:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password
|
||||
```json
|
||||
// Welcome script
|
||||
{
|
||||
"source": "/yourpath/.welcome.sh",
|
||||
"target": "/root/.welcome.sh",
|
||||
"type": "bind"
|
||||
},
|
||||
```
|
||||
|
||||
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
|
||||
1. In the VS code command palette, rebuild the container.
|
||||
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||
|
||||
+110
-80
@@ -1,81 +1,111 @@
|
||||
{
|
||||
"name": "gluetun-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
||||
"workspaceFolder": "/workspace",
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"eamodio.gitlens", // IDE Git information
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||
"bierner.emojisense", // Emoji sense for markdown
|
||||
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||
"github.vscode-pull-request-github", // Github interaction
|
||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||
"IBM.output-colorizer", // Colorize your output/test logs
|
||||
"mohsen1.prettify-json", // Prettify JSON data
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
"editor.codeActionsOnSaveTimeout": 3000,
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
// Optional: Disable snippets, as they conflict with completion ranking.
|
||||
"editor.snippetSuggestions": "none"
|
||||
},
|
||||
"[go.mod]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true
|
||||
},
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags=",
|
||||
// "CGO_ENABLED": 1 // for the race detector
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"go.testEnvVars": {
|
||||
"": ""
|
||||
},
|
||||
"go.testFlags": [
|
||||
"-v",
|
||||
// "-race"
|
||||
],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnTestPackage": true
|
||||
}
|
||||
{
|
||||
"name": "gluetun-dev",
|
||||
// User defined settings
|
||||
"containerEnv": {
|
||||
"TZ": ""
|
||||
},
|
||||
// Fixed settings
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile"
|
||||
},
|
||||
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||
"capAdd": [
|
||||
"NET_ADMIN", // Gluetun specific
|
||||
"SYS_PTRACE" // for dlv Go debugging
|
||||
],
|
||||
"securityOpt": [
|
||||
"seccomp=unconfined" // for dlv Go debugging
|
||||
],
|
||||
"mounts": [
|
||||
// Zsh commands history persistence
|
||||
{
|
||||
"source": "${localEnv:HOME}/.zsh_history",
|
||||
"target": "/root/.zsh_history",
|
||||
"type": "bind"
|
||||
},
|
||||
// Git configuration file
|
||||
{
|
||||
"source": "${localEnv:HOME}/.gitconfig",
|
||||
"target": "/root/.gitconfig",
|
||||
"type": "bind"
|
||||
},
|
||||
// SSH directory for Linux, OSX and WSL
|
||||
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
// created in the container. On Windows, files are copied
|
||||
// from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
{
|
||||
"source": "${localEnv:HOME}/.ssh",
|
||||
"target": "/mnt/ssh",
|
||||
"type": "bind"
|
||||
},
|
||||
// Docker socket to access the host Docker server
|
||||
{
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"type": "bind"
|
||||
}
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"eamodio.gitlens", // IDE Git information
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||
"bierner.emojisense", // Emoji sense for markdown
|
||||
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||
"github.vscode-pull-request-github", // Github interaction
|
||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||
"IBM.output-colorizer", // Colorize your output/test logs
|
||||
"github.copilot" // AI code completion
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[go.mod]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"ui.diagnostic.analyses": {
|
||||
"ST1000": false
|
||||
},
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
"editor.formatOnSave": true,
|
||||
"go.buildTags": "linux",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"go.testEnvVars": {
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"go.testFlags": [
|
||||
"-v",
|
||||
"-race"
|
||||
],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnTestPackage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
build: .
|
||||
image: godevcontainer
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Docker configuration
|
||||
- ~/.docker:/root/.docker:z
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
- ~/.ssh:/root/.ssh:z
|
||||
# For Windows without WSL, a copy will be made
|
||||
# from /tmp/.ssh to ~/.ssh to fix permissions
|
||||
#- ~/.ssh:/tmp/.ssh:ro
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history:z
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig:z
|
||||
environment:
|
||||
- TZ=
|
||||
cap_add:
|
||||
# For debugging with dlv
|
||||
# - SYS_PTRACE
|
||||
- NET_ADMIN
|
||||
security_opt:
|
||||
# For debugging with dlv
|
||||
- seccomp:unconfined
|
||||
entrypoint: zsh -c "while sleep 1000; do :; done"
|
||||
@@ -7,3 +7,4 @@ Dockerfile
|
||||
LICENSE
|
||||
README.md
|
||||
title.svg
|
||||
devrun
|
||||
|
||||
@@ -4,15 +4,15 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository
|
||||
1. [Fork](https://github.com/passteque/gluetun/fork) and clone the repository
|
||||
1. Create a new branch `git checkout -b my-branch-name`
|
||||
1. Modify the code
|
||||
1. Ensure the docker build succeeds `docker build .` (you might need `export DOCKER_BUILDKIT=1`)
|
||||
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/passteque/gluetun/compare)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
|
||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
|
||||
@@ -7,13 +7,18 @@ body:
|
||||
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/Docker-image-tags) if that can help.
|
||||
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
@@ -40,10 +45,12 @@ body:
|
||||
attributes:
|
||||
label: VPN service provider
|
||||
options:
|
||||
- AirVPN
|
||||
- Custom
|
||||
- Cyberghost
|
||||
- ExpressVPN
|
||||
- FastestVPN
|
||||
- Giganews
|
||||
- HideMyAss
|
||||
- IPVanish
|
||||
- IVPN
|
||||
@@ -54,11 +61,12 @@ body:
|
||||
- PrivateVPN
|
||||
- ProtonVPN
|
||||
- PureVPN
|
||||
- SlickVPN
|
||||
- Surfshark
|
||||
- TorGuard
|
||||
- VPNSecure.me
|
||||
- VPNUnlimited
|
||||
- VyprVPN
|
||||
- WeVPN
|
||||
- Windscribe
|
||||
validations:
|
||||
required: true
|
||||
@@ -72,6 +80,7 @@ body:
|
||||
- Portainer
|
||||
- Kubernetes
|
||||
- Podman
|
||||
- Unraid
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
@@ -81,7 +90,7 @@ body:
|
||||
label: What is the version of Gluetun
|
||||
description: |
|
||||
Copy paste the version line at the top of your logs.
|
||||
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -94,9 +103,9 @@ body:
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Share your logs
|
||||
label: Share your logs (at least 10 lines)
|
||||
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
||||
render: log
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -1,7 +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
|
||||
url: https://github.com/passteque/gluetun/discussions/new/choose
|
||||
about: Please create a Github discussion.
|
||||
- name: Unraid template issue
|
||||
url: https://github.com/qdm12/gluetun/discussions/550
|
||||
url: https://github.com/passteque/gluetun/discussions/550
|
||||
about: Please read the relevant Github discussion.
|
||||
|
||||
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
||||
|
||||
---
|
||||
|
||||
One of the following is required:
|
||||
Important notes:
|
||||
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
|
||||
## For Wireguard
|
||||
|
||||
Wireguard can be natively supported ONLY if:
|
||||
|
||||
- the `PrivateKey` field value is the same across all servers for one user account
|
||||
- the `Address` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `PublicKey` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `Endpoint` field value:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||
- can be determined using a pattern, for example using country codes in hostnames
|
||||
|
||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||
|
||||
If **all** of these conditions are met, please provide an answer for each of them.
|
||||
|
||||
## For OpenVPN
|
||||
|
||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
|
||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||
you can only use a custom Openvpn configuration file.
|
||||
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
name: Wiki issue
|
||||
description: Report a Wiki issue
|
||||
title: "Wiki issue: "
|
||||
labels: ["📄 Wiki issue"]
|
||||
body:
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: "URL to the Wiki page"
|
||||
placeholder: "https://github.com/qdm12/gluetun/wiki/OpenVPN-options"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "What's the issue?"
|
||||
validations:
|
||||
required: true
|
||||
+31
-5
@@ -4,12 +4,38 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
interval: "weekly"
|
||||
- # Servers data dependency that should be updated as soon as
|
||||
# possible when a new version is released, to have the latest
|
||||
# servers available
|
||||
package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- # In particular avoid amneziawg-go which have v1.x.y versions available
|
||||
# on the Go modules proxy, but are not in the Github repository tags
|
||||
# and are not the latest releases either. Most likely a mistake from the
|
||||
# maintainers, which is persisted on the Go proxy.
|
||||
dependency-name: "github.com/amnezia-vpn/amneziawg-go"
|
||||
versions: ["1.x"]
|
||||
groups:
|
||||
low-importance:
|
||||
patterns:
|
||||
- "github.com/breml/rootcerts"
|
||||
- "github.com/fatih/color"
|
||||
- "github.com/golang/mock"
|
||||
- "github.com/klauspost/compress"
|
||||
- "github.com/klauspost/pgzip"
|
||||
- "github.com/pelletier/go-toml/v2"
|
||||
- "github.com/qdm12/goshutdown"
|
||||
- "github.com/qdm12/gosplash"
|
||||
- "github.com/qdm12/gotree"
|
||||
- "github.com/qdm12/log"
|
||||
- "github.com/stretchr/testify"
|
||||
- "github.com/ulikunitz/xz"
|
||||
- "gopkg.in/ini.v1"
|
||||
|
||||
+124
-78
@@ -1,110 +1,156 @@
|
||||
- name: "Bug :bug:"
|
||||
color: "b60205"
|
||||
description: ""
|
||||
- name: "Feature request :bulb:"
|
||||
color: "0e8a16"
|
||||
description: ""
|
||||
- name: "Help wanted :pray:"
|
||||
color: "4caf50"
|
||||
description: ""
|
||||
- name: "Documentation :memo:"
|
||||
color: "c5def5"
|
||||
description: ""
|
||||
- name: "Needs more info :thinking:"
|
||||
color: "795548"
|
||||
description: ""
|
||||
- name: "Status: 🗯️ Waiting for feedback"
|
||||
color: "f7d692"
|
||||
- name: "Status: 🔴 Blocked"
|
||||
color: "f7d692"
|
||||
description: "Blocked by another issue or pull request"
|
||||
- name: "Status: 📌 Before next release"
|
||||
color: "f7d692"
|
||||
description: "Has to be done before the next release"
|
||||
- name: "Status: 🔒 After next release"
|
||||
color: "f7d692"
|
||||
description: "Will be done after the next release"
|
||||
- name: "Status: 🟡 Nearly resolved"
|
||||
color: "f7d692"
|
||||
description: "This might be resolved or is about to be resolved"
|
||||
|
||||
# Priority
|
||||
- name: "🚨 Urgent"
|
||||
color: "d5232f"
|
||||
description: ""
|
||||
- name: "💤 Low priority"
|
||||
color: "4285f4"
|
||||
description: ""
|
||||
- name: "Closed: ⚰️ Inactive"
|
||||
color: "959a9c"
|
||||
description: "No answer was received for weeks"
|
||||
- name: "Closed: 👥 Duplicate"
|
||||
color: "959a9c"
|
||||
description: "Issue duplicates an existing issue"
|
||||
- name: "Closed: 🗑️ Bad issue"
|
||||
color: "959a9c"
|
||||
- name: "Closed: ☠️ cannot be done"
|
||||
color: "959a9c"
|
||||
- name: "Closed: 🤖🍺 drunk AI"
|
||||
color: "959a9c"
|
||||
|
||||
- name: "Priority: 🚨 Urgent"
|
||||
color: "03adfc"
|
||||
- name: "Priority: 💤 Low priority"
|
||||
color: "03adfc"
|
||||
|
||||
- name: "Complexity: ☣️ Hard to do"
|
||||
color: "ff9efc"
|
||||
- name: "Complexity: 🟩 Easy to do"
|
||||
color: "ff9efc"
|
||||
|
||||
- name: "Popularity: ❤️🔥 extreme"
|
||||
color: "ffc7ea"
|
||||
- name: "Popularity: ❤️ high"
|
||||
color: "ffc7ea"
|
||||
|
||||
# VPN providers
|
||||
- name: ":cloud: Cyberghost"
|
||||
- name: "☁️ AirVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: HideMyAss"
|
||||
- name: "☁️ Custom"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: IPVanish"
|
||||
- name: "☁️ Cyberghost"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: IVPN"
|
||||
- name: "☁️ Giganews"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: ExpressVPN"
|
||||
- name: "☁️ HideMyAss"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: FastestVPN"
|
||||
- name: "☁️ IPVanish"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Mullvad"
|
||||
- name: "☁️ IVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: NordVPN"
|
||||
- name: "☁️ ExpressVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Perfect Privacy"
|
||||
- name: "☁️ FastestVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: PIA"
|
||||
- name: "☁️ Mullvad"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Privado"
|
||||
- name: "☁️ NordVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: PrivateVPN"
|
||||
- name: "☁️ Perfect Privacy"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: ProtonVPN"
|
||||
- name: "☁️ PIA"
|
||||
color: "cfe8d4"
|
||||
- name: ":cloud: PureVPN"
|
||||
- name: "☁️ Privado"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Surfshark"
|
||||
- name: "☁️ PrivateVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Torguard"
|
||||
- name: "☁️ ProtonVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: VPNUnlimited"
|
||||
- name: "☁️ PureVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Vyprvpn"
|
||||
- name: "☁️ SlickVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: WeVPN"
|
||||
- name: "☁️ Surfshark"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Windscribe"
|
||||
- name: "☁️ Torguard"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ VPNSecure.me"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ VPNUnlimited"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Vyprvpn"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Windscribe"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
|
||||
# Problem category
|
||||
- name: "Openvpn"
|
||||
- name: "Category: User error 🤦"
|
||||
from_name: "Category: Config problem 📝"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Wireguard"
|
||||
- name: "Category: Healthcheck 🩺"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Unbound (DNS over TLS)"
|
||||
- name: "Category: Documentation ✒️"
|
||||
description: "A problem with the readme or a code comment."
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Firewall"
|
||||
- name: "Category: Maintenance ⛓️"
|
||||
description: "Anything related to code or other maintenance"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "HTTP proxy"
|
||||
- name: "Category: Logs 📚"
|
||||
description: "Something to change in logs"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Shadowsocks"
|
||||
- name: "Category: Good idea 🎯"
|
||||
description: "This is a good idea, judged by the maintainers"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Healthcheck server"
|
||||
- name: "Category: Motivated! 🙌"
|
||||
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
|
||||
color: "ffc7ea"
|
||||
description: ""
|
||||
- name: "Control server"
|
||||
- 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: MTU discovery 🔦"
|
||||
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: Socks5 proxy 🔁"
|
||||
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"
|
||||
description: ""
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Description
|
||||
|
||||
<!-- Please describe the reason for the changes being proposed. -->
|
||||
|
||||
# Issue
|
||||
|
||||
<!-- Please link to the issue(s) this change relates to. -->
|
||||
|
||||
# Assertions
|
||||
|
||||
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
||||
@@ -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
|
||||
+139
-35
@@ -17,8 +17,6 @@ on:
|
||||
- go.mod
|
||||
- go.sum
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/ci.yml
|
||||
- cmd/**
|
||||
@@ -30,30 +28,34 @@ on:
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
|
||||
if: |
|
||||
github.actor != 'dependabot[bot]' &&
|
||||
(github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
locale: "US"
|
||||
level: error
|
||||
exclude: |
|
||||
./.golangci.yml
|
||||
*.md
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Go mod tidy check
|
||||
run: docker build --target tidy .
|
||||
- name: Mocks check
|
||||
run: docker build --target mocks .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
@@ -61,69 +63,171 @@ jobs:
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Code security analysis
|
||||
uses: snyk/actions/golang@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
- name: Verify dev cross platform compatibility
|
||||
run: docker build --target xcompile .
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
|
||||
# - name: Image security analysis
|
||||
# uses: snyk/actions/docker@master
|
||||
# env:
|
||||
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
# with:
|
||||
# image: final-image
|
||||
verify-tools:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
publish:
|
||||
# Only run if it's a push event or if it's a PR from this repository
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./devrun/go.mod
|
||||
- run: go test ./...
|
||||
working-directory: ./devrun
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./ci/go.mod
|
||||
- run: go test ./...
|
||||
working-directory: ./ci
|
||||
|
||||
verify-private:
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
||||
github.repository == 'passteque/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]
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- run: docker build -t qmcgaw/gluetun .
|
||||
|
||||
- name: Setup Go for CI utility
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ci/go.mod
|
||||
|
||||
- name: Build utility
|
||||
run: go build -C ./ci -o runner ./cmd/main.go
|
||||
|
||||
- name: Run Gluetun container with Mullvad configuration
|
||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{
|
||||
secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
||||
|
||||
- name: Run Gluetun container with ProtonVPN Wireguard and port forwarding
|
||||
configuration
|
||||
run:
|
||||
echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner
|
||||
protonvpn-wireguard-port-forwarding
|
||||
|
||||
- name: Run Gluetun container with ProtonVPN OpenVPN and port forwarding
|
||||
configuration
|
||||
run: echo -e "${{ secrets.PROTONVPN_OPENVPN_USER }}\n${{
|
||||
secrets.PROTONVPN_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||
protonvpn-openvpn-port-forwarding
|
||||
|
||||
# - name:
|
||||
# Run Gluetun container with Private Internet Access OpenVPN and port
|
||||
# forwarding configuration
|
||||
# run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
|
||||
# secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||
# private-internet-access-openvpn-port-forwarding
|
||||
|
||||
- name: Run Gluetun container with AirVPN Wireguard configuration
|
||||
run: echo -e "${{ secrets.AIRVPN_WIREGUARD_PRIVATE_KEY }}\n${{
|
||||
secrets.AIRVPN_WIREGUARD_PRESHARED_KEY }}\n${{
|
||||
secrets.AIRVPN_WIREGUARD_ADDRESSES }}" | ./ci/runner airvpn-wireguard
|
||||
|
||||
- name: Run Gluetun container with AirVPN OpenVPN configuration
|
||||
run:
|
||||
echo -e "${{ secrets.AIRVPN_OPENVPN_KEY }}\n${{ secrets.AIRVPN_OPENVPN_CERT
|
||||
}}" | ./ci/runner airvpn-openvpn
|
||||
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
- uses: github/codeql-action/autobuild@v4
|
||||
- uses: github/codeql-action/analyze@v4
|
||||
|
||||
publish:
|
||||
if: |
|
||||
github.repository == 'passteque/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, verify-private, codeql]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v6
|
||||
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=branch,enable=${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
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@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: qdm12
|
||||
password: ${{ secrets.GHCR_PAT }}
|
||||
|
||||
- name: Short commit
|
||||
id: shortcommit
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
run: |
|
||||
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
|
||||
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
SHORT_SHA=
|
||||
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v2.8.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Closed issue
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Closed issues are **NOT** monitored, so commenting here will likely NOT be seen.
|
||||
If you think this is *still unresolved* and have **more information** to bring, please either
|
||||
re-open this issue or create another issue.
|
||||
|
||||
❤️😠 temporarily help the Gluetun community and fight the AI slop scam website `gluetun[dot]com` by setting `BORINGPOLL_GLUETUNCOM=on` on the latest image.
|
||||
See [the option in the wiki for more information](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/others.md)
|
||||
|
||||
This is an automated comment setup because @qdm12 is the sole maintainer of this project
|
||||
which became too popular to monitor closed issues.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
{
|
||||
"pattern": "^https://console.substack.com/p/console-72$"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://github.com/passteque/gluetun$"
|
||||
}
|
||||
],
|
||||
"timeout": "20s",
|
||||
"retryOn429": false,
|
||||
"fallbackRetryDelay": "30s",
|
||||
"aliveStatusCodes": [
|
||||
200,
|
||||
429
|
||||
]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Dependabot
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/dependabot.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Docker Hub description
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- README.md
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.4.0
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
repository: qmcgaw/gluetun
|
||||
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
||||
readme-filepath: README.md
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Fork
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/fork.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
@@ -7,9 +7,11 @@ on:
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: crazy-max/ghaction-github-labeler@v3
|
||||
- uses: actions/checkout@v6
|
||||
- uses: crazy-max/ghaction-github-labeler@v6
|
||||
with:
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Markdown
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
markdown:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v22
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint-cli2.jsonc
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
locale: "US"
|
||||
level: error
|
||||
pattern: |
|
||||
*.md
|
||||
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: yes
|
||||
config-file: .github/workflows/configs/mlc-config.json
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v5
|
||||
if: github.repository == 'passteque/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
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Opened issue
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
@qdm12 is more or less the only maintainer of this project and works on it in his free time.
|
||||
Please:
|
||||
- **do not** ask for updates, be patient
|
||||
- :+1: the issue to show your support instead of commenting
|
||||
@qdm12 usually checks issues at least once a week, if this is a new urgent bug,
|
||||
[revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md)
|
||||
@@ -1 +1,2 @@
|
||||
scratch.txt
|
||||
.DS_Store
|
||||
|
||||
+98
-57
@@ -1,66 +1,106 @@
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
version: "2"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- maligned
|
||||
- goerr113
|
||||
- containedctx
|
||||
- path: internal/server/
|
||||
linters:
|
||||
- dupl
|
||||
- path: internal/configuration/
|
||||
linters:
|
||||
- dupl
|
||||
- path: internal/constants/
|
||||
linters:
|
||||
- dupl
|
||||
- text: "exported: exported var Err*"
|
||||
linters:
|
||||
- revive
|
||||
- text: "mnd: Magic number: 0644*"
|
||||
linters:
|
||||
- gomnd
|
||||
- text: "mnd: Magic number: 0400*"
|
||||
linters:
|
||||
- gomnd
|
||||
- text: "variable 'mssFix' is only used in the if-statement*"
|
||||
path: "openvpnconf.go"
|
||||
linters:
|
||||
- ifshort
|
||||
- text: "variable 'auth' is only used in the if-statement*"
|
||||
path: "openvpnconf.go"
|
||||
linters:
|
||||
- ifshort
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate "
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
run:
|
||||
build-tags:
|
||||
- integration
|
||||
|
||||
linters:
|
||||
settings:
|
||||
misspell:
|
||||
locale: US
|
||||
goconst:
|
||||
ignore-string-values:
|
||||
# commonly used settings strings
|
||||
- "^disabled$"
|
||||
# Firewall and routing strings
|
||||
- "^(ACCEPT|DROP)$"
|
||||
- "^--append$"
|
||||
- "^--delete$"
|
||||
- "^all$"
|
||||
- "^(tcp|udp)$"
|
||||
# Server route strings
|
||||
- "^/status$"
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- err113
|
||||
- maintidx
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
path: internal\/server\/.+\.go
|
||||
- linters:
|
||||
- ireturn
|
||||
text: returns interface \(golang\.org\/x\/sys\/unix\.Sockaddr\)
|
||||
- linters:
|
||||
- ireturn
|
||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
||||
- linters:
|
||||
- revive
|
||||
path: internal\/provider\/(common|utils)\/.+\.go
|
||||
text: "var-naming: avoid (bad|meaningless) package names"
|
||||
- linters:
|
||||
- lll
|
||||
source: "^// https://.+$"
|
||||
- linters:
|
||||
- mnd
|
||||
source: "^ cleanups\\.Add.+$"
|
||||
path: internal\/(wireguard|amneziawg)\/run\.go
|
||||
- linters:
|
||||
- err113
|
||||
- mnd
|
||||
path: ci\/.+\.go
|
||||
- linters:
|
||||
- err113
|
||||
text: "do not define dynamic errors, use wrapped static errors instead"
|
||||
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
enable:
|
||||
# - cyclop
|
||||
# - errorlint
|
||||
# - ireturn
|
||||
# - varnamelen
|
||||
# - wrapcheck
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- err113
|
||||
- errchkjson
|
||||
- errname
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- fatcontext
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
@@ -68,43 +108,44 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- grouper
|
||||
- ifshort
|
||||
- importas
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tenv
|
||||
- tagalign
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
- doc
|
||||
- zerologlint
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
},
|
||||
"ignores": [
|
||||
".github/pull_request_template.md"
|
||||
]
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
// This list should be kept to the strict minimum
|
||||
// to develop this project.
|
||||
"recommendations": [
|
||||
"golang.go",
|
||||
"davidanson.vscode-markdownlint",
|
||||
],
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
// The settings should be kept to the strict minimum
|
||||
// to develop this project.
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"go.buildTags": "linux,integration",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"go.testEnvVars": {
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"go.testFlags": [
|
||||
"-v",
|
||||
"-race"
|
||||
],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnTestPackage": true,
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package"
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Update a VPN provider servers data",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"run",
|
||||
"./cmd/gluetun/main.go",
|
||||
"update",
|
||||
"${input:updateMode}",
|
||||
"-providers",
|
||||
"${input:provider}"
|
||||
],
|
||||
},
|
||||
{
|
||||
"label": "Add a Gluetun Github Git remote",
|
||||
"type": "shell",
|
||||
"command": "git",
|
||||
"args": [
|
||||
"remote",
|
||||
"add",
|
||||
"${input:githubRemoteUsername}",
|
||||
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
||||
],
|
||||
},
|
||||
{
|
||||
"label": "Devrun",
|
||||
"type": "shell",
|
||||
"command": "go run ./cmd/main.go run ${input:devrunProvider} ${input:devrunVPNProtocol} ${input:devrunExtraFlags}",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/devrun"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "provider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a provider (or comma separated list of providers)",
|
||||
},
|
||||
{
|
||||
"id": "updateMode",
|
||||
"type": "pickString",
|
||||
"description": "Update mode to use",
|
||||
"options": [
|
||||
"-maintainer",
|
||||
"-enduser"
|
||||
],
|
||||
"default": "-maintainer"
|
||||
},
|
||||
{
|
||||
"id": "githubRemoteUsername",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a Github username",
|
||||
},
|
||||
{
|
||||
"id": "devrunProvider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a single provider",
|
||||
},
|
||||
{
|
||||
"id": "devrunVPNProtocol",
|
||||
"type": "pickString",
|
||||
"description": "VPN protocol to use",
|
||||
"options": [
|
||||
"wireguard",
|
||||
"openvpn"
|
||||
],
|
||||
"default": "wireguard"
|
||||
},
|
||||
{
|
||||
"id": "devrunExtraFlags",
|
||||
"type": "promptString",
|
||||
"description": "Extra flags (optional)",
|
||||
"default": ""
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# AGENTS
|
||||
|
||||
Guidance for coding agents working in this repository.
|
||||
|
||||
## Scope and priorities
|
||||
|
||||
- Keep changes minimal and targeted. Feel free to do light refactors that are relevant to the modifications.
|
||||
- Breaking changes:
|
||||
- Do not introduce breaking usage behavior (cli flags, environment variables, etc.) unless explicitly agreed.
|
||||
- Do not introduce breaking changes for the Go API in the `pkg/` directory.
|
||||
- If a compatibility break seems beneficial, stop and ask for confirmation before implementing it.
|
||||
- Update or add tests when behavior changes.
|
||||
|
||||
## Go coding conventions
|
||||
|
||||
### General guidelines
|
||||
|
||||
- Use explicit, descriptive variable names by default.
|
||||
- Notable bad examples: `req`, `resp`, `cfg`, `v`
|
||||
- Allowed short-name exceptions:
|
||||
- indexes such as `i`, `j`
|
||||
- `ctx` for `context.Context`
|
||||
- `t` for `*testing.T` and `b` for `*testing.B`
|
||||
- `ctrl` for `*gomock.Controller`
|
||||
- `err` for `error`, `errs` for `[]error`
|
||||
- `wg` for `*sync.WaitGroup`
|
||||
- Avoid using global variables except for:
|
||||
- exported sentinel errors that are used outside the package boundaries
|
||||
- regular expressions defined with `regexp.MustCompile`
|
||||
- variables set by the build pipeline, such as `Version` and `BuildDate`
|
||||
- Constants
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Each one should be defined right above where it's used, instead of having multiple defined at the same place in a `const ()` block
|
||||
- If one is only used in a single production code function, define it right above it so it's more local for readability.
|
||||
- Do not define constants when constants exist in other packages, for example `http.StatusBadRequest` or `log.LevelDebug`.
|
||||
- Structs
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Do not use the short if form, prefer the longer one
|
||||
- Follow modern Go, according to the Go version defined in go.mod. Prefer modern constructs when equivalent:
|
||||
- Example: use `for i := range 5` rather than `for i := 0; i < 5; i++`.
|
||||
- Example: use `new("string")` rather than helper wrappers such as `stringPtr("string")`.
|
||||
- Example: no need to pin variables in for loops when using them in goroutines or subtests.
|
||||
- Use `New(...) *Item` constructor per package. Each package should ideally only have one constructor, although this is not a strict rule. The constructor should return a pointer to the struct, and not an interface.
|
||||
- Always prefer using context-aware functions, for example:
|
||||
- `exec.CommandContext` rather than `exec.Command`
|
||||
- `http.NewRequestWithContext` rather than `http.NewRequest`
|
||||
- Never export a symbol unless absolutely necessary.
|
||||
- Always use the most restrictive builtin types. For example prefer `uint` over `int` if it's only zero or positive. Prefer `uint16` is the max value is 65535.
|
||||
- Prefer using builtin types whenever possible AND do not define single field structs unless necessary
|
||||
- Prefer splitting a code line only when it triggers the `lll` linter, do not split a command or arguments list for each element
|
||||
- Use `netip` types instead of `net` types whenever possible
|
||||
- Use constants instead of variables whenever possible, especially function-local inline constants.
|
||||
- Do not use `time.Sleep`, prefer using a `time.Timer` with a `select` statement also listening on a context cancelation
|
||||
- `panic`:
|
||||
- should only be used when a programming error is encountered and you should NOT return errors for programming errors (such as passing nil objects)
|
||||
- Its counterpart `recover` should not really be used, except for testing a panic in test code (or use `assert.PanicsWithValue`).
|
||||
|
||||
### Directory structure and file naming
|
||||
|
||||
- Executable main packages with a single `main()` function must be in the `cmd` directory.
|
||||
Prefer having top level logic and have a longer `main()` function rather than having an `internal/app` package.
|
||||
- Code lives by default in subpackages within the `internal` directory
|
||||
- Code needing to be imported by external Go modules must be in subpackages within the `pkg` directory
|
||||
- Example code especially using the `pkg` directory must be in `main` packages within the `examples` directory, each with a single `main.go` function.
|
||||
- If AND only if the repository is a Go library and not a Go application, you may have Go files at the root of the project to simplify import paths. Most of the code should still be in subpackages in the `internal` directory.
|
||||
- Interfaces should be defined in `interfaces.go` files for each package. If there are unexported interfaces which need to be mocked, which is rare, they should be defined in `interfaces_local.go` files.
|
||||
- Mock files are
|
||||
- `mocks_generate_test.go` which only contains `//go:generate` directives for generating mocks, and no actual code
|
||||
- `mocks_test.go` which contains the generated mocks from exported interfaces and no other code, and is ignored in coverage reports
|
||||
- `mocks_local_test.go` (rare) which contains the generated mocks from unexported interfaces and no other code, and is ignored in coverage reports
|
||||
- NEVER generate an exported mock in a non test file, prefer re-generating files across packages.
|
||||
- Package naming
|
||||
- Your package name should be the same as the directory containing it, **except for the `main` package**
|
||||
- Use single words for package names
|
||||
- Do not use generic names for package names such as `utils` or `helpers`
|
||||
- Package nesting
|
||||
- Try to avoid nesting packages by default
|
||||
- You can nest packages if you have different implementations for the same interface (e.g. a store interface)
|
||||
- You can nest packages if you start having a lot of Go files (more than 10) and it really does make sense to make subpackages
|
||||
|
||||
### Linting
|
||||
|
||||
The linter is `golangci-lint` with the configuration defined in `.golangci.yml`.
|
||||
|
||||
To exclude code from linting, prefer using, when absolutely necessary, command comments `//nolint:<linter>`.
|
||||
This allows the `nolintlint` linter to detect and report unnecessary `//nolint` comments later.
|
||||
You can notably use `//nolint:lll` and, for good valid reasons, `//nolint:gosec`. Sometimes `//nolint:mnd` when it just doesn't make sense to extract a constant such as `n = n << 4`
|
||||
Always prefer placing `//nolint` comments on the same code line where the error comes from, and not above a code block.
|
||||
|
||||
### Mocking
|
||||
|
||||
Mocking works with the `go.uber.org/mock` library, and the `mockgen` tool.
|
||||
|
||||
- Mocks from exported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Mocks from unexported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_local_test.go` files. The source file for unexported interfaces is `interfaces_local.go`. The go generate command is similar to:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package $GOPACKAGE -source interfaces_local.go
|
||||
```
|
||||
|
||||
- Mocks from external interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_<package-name>_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_<package-name>_test.go -package $GOPACKAGE module-name InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Generated mocks usage in tests:
|
||||
- Define mocks in the subtest, not in the parent test. You can also have a function returning the mocks as a field of the test case struct, which takes in the subtest `*testing.T` as argument, and call it in the subtest to get the mocks.
|
||||
- **Never** use `gomock.Any()` as argument. Always use concrete, precise arguments. You might need to define a custom GoMock matcher for your argument in some very niche and corner cases.
|
||||
- **Never** use `.AnyTimes()` on mocks. Always define the number of times a certain mock call should be called, with `.Times(3)` for example.
|
||||
- **Always** set the `.Return(...)` on the mock if the function returns something.
|
||||
- Avoid using **mock helpers** functions, prefer a bit of repetition than tight coupling and dependency
|
||||
|
||||
### main.go
|
||||
|
||||
- Make the program OS signal aware, so it attempts a graceful shutdown when interruped. Force quit the program on a second interrupt signal.
|
||||
|
||||
### Formatting
|
||||
|
||||
The Go formatter used is gofumpt.
|
||||
|
||||
### Errors
|
||||
|
||||
- Always prefer wrapping errors with some context with `fmt.Errorf("doing this: %w", err)`
|
||||
- In rare cases, you can just use `return err` notably:
|
||||
- If the function is called **recursively**, since we don't wrap the wrapping multiple times for each recursion
|
||||
- If the current function only statement is the call to another function, for example:
|
||||
|
||||
```go
|
||||
func (s *Struct) Fetch() error {
|
||||
return fetch() // do not wrap the error
|
||||
}
|
||||
```
|
||||
|
||||
- When wrapping errors, use verbs ending in "ing" and no "failed to" or "cannot" to avoid redundancy. For example, use `fmt.Errorf("resolving host: %w", err)` rather than `fmt.Errorf("failed to resolve host: %w", err)`.
|
||||
- When wrapping an error, the context should NEVER contain variables injected as arguments in the function returning an error, to avoid repeating the same variable in multiple error messages.
|
||||
- Testing errors:
|
||||
- If the error does not wrap a sentinel error, use `assert.ErrorContains` to check for error messages, rather than `assert.EqualError`, to avoid having to update tests for minor changes in error messages. And use `assert.NoError` to check for no error.
|
||||
- If the error wraps a sentinel error, use `assert.ErrorIs` to check both for the sentinel error or an expected nil error. You can also check the error message with `assert.ErrorContains`
|
||||
|
||||
### User program settings
|
||||
|
||||
- For configuration structs, each field Go zero value (i.e. `0` for `int`, `nil` for `*string`) should be an INVALID value in the user sense. This is used to detect when a field is not set, in order to default it, merge it or override it. For example if `""` is not a valid value, the field should be of type `string`. Conversely, if `""` is a valid value, the field should be of type `*string` to distinguish between "not set" and "set to empty string". Notably, boolean fields are ALWAYS of type `*bool` for this reason, since both `true` and `false` are valid values.
|
||||
- Configuration reading and handling relies on the Go library github.com/qdm12/gosettings please use it whenever appropriate.
|
||||
- Do not wrap errors coming from `reader.Reader` methods, since they already contain the necessary context.
|
||||
- All keys passed to `reader.Reader` methods must be in environment variable format, i.e. uppercase with underscores. These get converted to lowercase and dashes for flags notably.
|
||||
- For each settings structs, define the following methods, which are usually unexported, but can be exported especially for the top level Settings struct, in this order:
|
||||
- `func (s *Settings) setDefaults()` whichs sets defaults (using `gosettings.Default*` functions) on unset fields
|
||||
- If the settings need to be patched at runtime, which is rarely the case, define `func (s *Settings) overrideWith(other Settings)` which overrides the settings with another settings struct, only for fields that are set in the other struct (using `gosettings.OverrideWith` functions).
|
||||
- `func (s Settings) validate() error` which validates the settings, and returns an error if anything is invalid
|
||||
- `func (s *Settings) read(r *reader.Reader) error` which reads the settings from a gosettings/reader.Reader (which can be from multiple sources, such as environment variables, cli flags, config files etc.)
|
||||
- `func (s Settings) String() string` which uses `toLinesNode().String()` to return a string representation of the settings
|
||||
- `func (s Settings) toLinesNode() *gotree.Node` which a github.com/qdm12/gotree `*Node` representing the settings
|
||||
|
||||
### Testing
|
||||
|
||||
- Use the github.com/stretchr/testify library for assertions
|
||||
- Most tests should be table tests with parallel subtests
|
||||
- Prefer map-based table tests of the form `map[string]struct{ ... }`, with the key as the test name.
|
||||
Use underscores in test names, not spaces, to keep `go test` output searchable.
|
||||
- Use `testCases` for the table variable name, and `testCase` for each iterated case value.
|
||||
- Run all tests in parallel:
|
||||
- call `t.Parallel()` in the top-level test
|
||||
- call `t.Parallel()` in each subtest
|
||||
|
||||
### Libraries to use
|
||||
|
||||
- Logging: `github.com/qdm12/log`
|
||||
- Splash information at program start: `github.com/qdm12/gosplash`
|
||||
- Long running services (i.e. health server, http prod server, backup loop etc.): `github.com/qdm12/goservices`
|
||||
- String tree structures: `github.com/qdm12/gotree`
|
||||
|
||||
### Extra rules
|
||||
|
||||
- Do not use `http.DefaultClient`, use a custom `*http.Client` with a fixed timeout and share with dependency injections.
|
||||
- Do not check for injected dependencies being `nil`, prefer to just panic on a nil pointer. By default it's fine to panic if a developer injects a dependency `nil`. `nil` does not mean use a default.
|
||||
|
||||
## Validation checklist
|
||||
|
||||
Run the following before finishing changes:
|
||||
|
||||
1. Go building `go build ./...`
|
||||
1. Go linting `golangci-lint run`
|
||||
1. Go unit tests `go test ./...`
|
||||
1. If a module is added or modified, run `go mod tidy`
|
||||
1. If an interface or mock command is modified, run `go generate -run mockgen ./...`
|
||||
|
||||
If a Markdown file is modified and `markdownlint-cli2` is available, run `markdownlint-cli2 "**/*.md"`
|
||||
|
||||
If a command is unavailable in the current environment, report it clearly and provide the exact command needed once available.
|
||||
+153
-57
@@ -1,18 +1,22 @@
|
||||
ARG ALPINE_VERSION=3.15
|
||||
ARG GO_ALPINE_VERSION=3.15
|
||||
ARG GO_VERSION=1.17
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.44.2
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG GO_ALPINE_VERSION=3.23
|
||||
ARG GO_VERSION=1.25
|
||||
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
||||
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
RUN apk --update add git g++
|
||||
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
|
||||
RUN apk --update add git g++ findutils iptables
|
||||
ENV CGO_ENABLED=0
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
COPY --from=mockgen /bin /go/bin/mockgen
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
@@ -28,16 +32,23 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
RUN golangci-lint run
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS tidy
|
||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||
RUN git init && \
|
||||
git config user.email ci@localhost && \
|
||||
git config user.name ci && \
|
||||
git add -A && git commit -m ci && \
|
||||
sed -i '/\/\/ indirect/d' go.mod && \
|
||||
go mod tidy && \
|
||||
git diff --exit-code -- go.mod
|
||||
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 xcompile
|
||||
RUN GOOS=darwin go build -o /dev/null ./...
|
||||
RUN GOOS=windows go build -o /dev/null ./...
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS build
|
||||
ARG TARGETPLATFORM
|
||||
@@ -61,92 +72,163 @@ LABEL \
|
||||
org.opencontainers.image.created=$CREATED \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$COMMIT \
|
||||
org.opencontainers.image.url="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.url="https://github.com/passteque/gluetun" \
|
||||
org.opencontainers.image.documentation="https://github.com/passteque/gluetun" \
|
||||
org.opencontainers.image.source="https://github.com/passteque/gluetun" \
|
||||
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
|
||||
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
|
||||
ENV VPN_SERVICE_PROVIDER=pia \
|
||||
VPN_TYPE=openvpn \
|
||||
# Common VPN options
|
||||
VPN_ENDPOINT_IP= \
|
||||
VPN_ENDPOINT_PORT= \
|
||||
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.5 \
|
||||
OPENVPN_VERSION=2.6 \
|
||||
OPENVPN_VERBOSITY=1 \
|
||||
OPENVPN_FLAGS= \
|
||||
OPENVPN_CIPHERS= \
|
||||
OPENVPN_AUTH= \
|
||||
OPENVPN_PROCESS_USER= \
|
||||
OPENVPN_IPV6=off \
|
||||
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= \
|
||||
WIREGUARD_IMPLEMENTATION=auto \
|
||||
# Amnezia
|
||||
AMNEZIAWG_ENDPOINT_IP= \
|
||||
AMNEZIAWG_ENDPOINT_PORT= \
|
||||
AMNEZIAWG_CONF_SECRETFILE=/run/secrets/wg0.conf \
|
||||
AMNEZIAWG_PRIVATE_KEY= \
|
||||
AMNEZIAWG_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
|
||||
AMNEZIAWG_PRESHARED_KEY= \
|
||||
AMNEZIAWG_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
|
||||
AMNEZIAWG_PUBLIC_KEY= \
|
||||
AMNEZIAWG_ALLOWED_IPS= \
|
||||
AMNEZIAWG_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||
AMNEZIAWG_ADDRESSES= \
|
||||
AMNEZIAWG_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||
AMNEZIAWG_MTU= \
|
||||
AMNEZIAWG_JC=0 \
|
||||
AMNEZIAWG_JMIN=0 \
|
||||
AMNEZIAWG_JMAX=0 \
|
||||
AMNEZIAWG_S1=0 \
|
||||
AMNEZIAWG_S2=0 \
|
||||
AMNEZIAWG_S3=0 \
|
||||
AMNEZIAWG_S4=0 \
|
||||
AMNEZIAWG_H1= \
|
||||
AMNEZIAWG_H2= \
|
||||
AMNEZIAWG_H3= \
|
||||
AMNEZIAWG_H4= \
|
||||
AMNEZIAWG_I1= \
|
||||
AMNEZIAWG_I2= \
|
||||
AMNEZIAWG_I3= \
|
||||
AMNEZIAWG_I4= \
|
||||
AMNEZIAWG_I5= \
|
||||
# VPN server port forwarding
|
||||
VPN_PORT_FORWARDING=off \
|
||||
VPN_PORT_FORWARDING_PROVIDER= \
|
||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||
VPN_PORT_FORWARDING_LISTENING_PORTS=0 \
|
||||
VPN_PORT_FORWARDING_PORTS_COUNT=1 \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
# PMTUD
|
||||
PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8 \
|
||||
PMTUD_TCP_ADDRESSES=1.1.1.1:443,8.8.8.8:443,1.1.1.1:53,8.8.8.8:53,[2606:4700:4700::1111]:53,[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:443,[2001:4860:4860::8888]:443 \
|
||||
# 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= \
|
||||
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \
|
||||
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
VPN_PORT_FORWARDING_USERNAME= \
|
||||
VPN_PORT_FORWARDING_PASSWORD= \
|
||||
# # 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 and ProtonVPN only:
|
||||
# # PIA only:
|
||||
SERVER_NAMES= \
|
||||
# # ProtonVPN only:
|
||||
# # 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=on \
|
||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||
FIREWALL_VPN_INPUT_PORTS= \
|
||||
FIREWALL_INPUT_PORTS= \
|
||||
FIREWALL_OUTBOUND_SUBNETS= \
|
||||
FIREWALL_DEBUG=off \
|
||||
FIREWALL_IPTABLES_LOG_LEVEL=info \
|
||||
# IPv6
|
||||
IPV6_CHECK_ADDRESSES=[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53 \
|
||||
# Logging
|
||||
LOG_LEVEL=info \
|
||||
# Health
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||
HEALTH_ADDRESS_TO_PING=github.com \
|
||||
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||
# DNS over TLS
|
||||
DOT=on \
|
||||
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: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_IPV6=off \
|
||||
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
|
||||
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
|
||||
HEALTH_SMALL_CHECK_TYPE=icmp \
|
||||
HEALTH_RESTART_VPN=on \
|
||||
# DNS
|
||||
DNS_SERVER=on \
|
||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||
# Note: DNS_UPSTREAM_RESOLVERS defaults to cloudflare in code if DNS_UPSTREAM_PLAIN_ADDRESSES is empty
|
||||
DNS_UPSTREAM_RESOLVERS= \
|
||||
DNS_BLOCK_IPS= \
|
||||
DNS_BLOCK_IP_PREFIXES= \
|
||||
DNS_CACHING=on \
|
||||
DNS_UPSTREAM_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
BLOCK_SURVEILLANCE=off \
|
||||
BLOCK_ADS=off \
|
||||
UNBLOCK= \
|
||||
DNS_UNBLOCK_HOSTNAMES= \
|
||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||
DNS_UPDATE_PERIOD=24h \
|
||||
DNS_ADDRESS=127.0.0.1 \
|
||||
DNS_KEEP_NAMESERVER=off \
|
||||
DNS_UPSTREAM_PLAIN_ADDRESSES= \
|
||||
# HTTP proxy
|
||||
HTTPPROXY= \
|
||||
HTTPPROXY_LOG=off \
|
||||
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
||||
HTTPPROXY_STEALTH=off \
|
||||
HTTPPROXY_USER= \
|
||||
HTTPPROXY_PASSWORD= \
|
||||
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||
@@ -158,14 +240,31 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
SHADOWSOCKS_PASSWORD= \
|
||||
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
||||
# Socks5
|
||||
SOCKS5_ENABLED=off \
|
||||
SOCKS5_LISTENING_ADDRESS=":1080" \
|
||||
SOCKS5_USER= \
|
||||
SOCKS5_PASSWORD= \
|
||||
# Control server
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PREFER_DIRECT_DOWNLOAD=no \
|
||||
UPDATER_PROTONVPN_EMAIL= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_PERIOD=12h \
|
||||
PUBLICIP_ENABLED=on \
|
||||
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
||||
PUBLICIP_API_TOKEN= \
|
||||
# Storage
|
||||
STORAGE_SERVERS_ENABLED=on \
|
||||
STORAGE_SERVERS_DIRECTORY_PATH=/gluetun/servers/ \
|
||||
# Pprof
|
||||
PPROF_ENABLED=no \
|
||||
PPROF_BLOCK_PROFILE_RATE=0 \
|
||||
@@ -174,22 +273,19 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Extras
|
||||
VERSION_INFORMATION=on \
|
||||
TZ= \
|
||||
PUID= \
|
||||
PGID=
|
||||
PUID=1000 \
|
||||
PGID=1000
|
||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
|
||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp 1080/tcp 1080/udp
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||
ARG TARGETPLATFORM
|
||||
RUN apk add --no-cache --update -l apk-tools && \
|
||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-r0 && \
|
||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
|
||||
apk del openvpn && \
|
||||
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
|
||||
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 && \
|
||||
# Fix vulnerability issue
|
||||
apk add --no-cache --update busybox && \
|
||||
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||
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 unbound && \
|
||||
mkdir /gluetun
|
||||
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
||||
|
||||
@@ -1,127 +1,138 @@
|
||||
# Gluetun VPN client
|
||||
|
||||
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
|
||||
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
|
||||
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
|
||||
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
|
||||
|
||||
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
|
||||
|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## Quick links
|
||||
|
||||
- [Setup](#Setup)
|
||||
- [Features](#Features)
|
||||
- Problem?
|
||||
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
|
||||
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
||||
- Suggestion?
|
||||
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
||||
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
||||
- Happy?
|
||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
||||
- Video:
|
||||
|
||||
[](https://youtu.be/0F6I03LQcI4)
|
||||
|
||||
- [Substack Console interview](https://console.substack.com/p/console-72)
|
||||
|
||||
## Features
|
||||
|
||||
- Based on Alpine 3.15 for a small Docker image of 29MB
|
||||
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **Mullvad**, **Ivpn** and **Windscribe**
|
||||
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
||||
- 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
|
||||
- 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
|
||||
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
|
||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
|
||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
|
||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||
- Unbound subprogram drops root privileges once launched
|
||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||
|
||||
## Setup
|
||||
|
||||
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
||||
|
||||
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
|
||||
|
||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
|
||||
|
||||
Here's a docker-compose.yml for the laziest:
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun
|
||||
# container_name: gluetun
|
||||
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
- 8388:8388/tcp # Shadowsocks
|
||||
- 8388:8388/udp # Shadowsocks
|
||||
volumes:
|
||||
- /yourpath:/gluetun
|
||||
environment:
|
||||
# See https://github.com/qdm12/gluetun/wiki
|
||||
- VPN_SERVICE_PROVIDER=ivpn
|
||||
- VPN_TYPE=openvpn
|
||||
# OpenVPN:
|
||||
- OPENVPN_USER=
|
||||
- OPENVPN_PASSWORD=
|
||||
# Wireguard:
|
||||
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
||||
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||
# Timezone for accurate log times
|
||||
- TZ=
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/qdm12/gluetun/master/LICENSE)
|
||||
# Gluetun VPN client
|
||||
|
||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
|
||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
||||
|
||||
🗯️ this repository will be migrated to [github.com/passteque/gluetun](https://github.com/passteque/gluetun) on 2026-05-21, which is a Github organization under my sole control, so don't get alarmed if you get redirected in the coming days 😉 Reason being migrating Github sponsors to the Open source collective due to my personal situation, basically annoying paperwork. On the plus side, it will be more transparent and funds donated will only be used for the project. The Docker image names will remain the same.
|
||||
|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## Quick links
|
||||
|
||||
- [Setup](#setup)
|
||||
- [Features](#features)
|
||||
- Problem?
|
||||
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
|
||||
- [Start a discussion](https://github.com/passteque/gluetun/discussions)
|
||||
- [Fix the Unraid template](https://github.com/passteque/gluetun/discussions/550)
|
||||
- Suggestion?
|
||||
- [Create an issue](https://github.com/passteque/gluetun/issues)
|
||||
- Happy?
|
||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
- 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)
|
||||
- Video:
|
||||
|
||||
[](https://youtu.be/0F6I03LQcI4)
|
||||
|
||||
- [Substack Console interview](https://console.substack.com/p/console-72)
|
||||
|
||||
## Features
|
||||
|
||||
- Based on Alpine 3.23 for a small Docker image of 43.1MB
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- More in progress, see [#134](https://github.com/passteque/gluetun/issues/134)
|
||||
- Supports AmneziaWG only with the custom provider 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`
|
||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||
- Built in Socks5 proxy server (tunnels TCP+UDP) - partial credits to @angelakis and @adjscent
|
||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||
- [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 🎆
|
||||
- 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
|
||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||
|
||||
## Setup
|
||||
|
||||
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
||||
|
||||
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||
|
||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
|
||||
|
||||
Here's a docker-compose.yml for the laziest:
|
||||
|
||||
```yml
|
||||
---
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun
|
||||
# container_name: gluetun
|
||||
# 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
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
- 8388:8388/tcp # Shadowsocks
|
||||
- 8388:8388/udp # Shadowsocks
|
||||
volumes:
|
||||
- /yourpath:/gluetun
|
||||
environment:
|
||||
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
||||
- VPN_SERVICE_PROVIDER=ivpn
|
||||
- VPN_TYPE=openvpn
|
||||
# OpenVPN:
|
||||
- OPENVPN_USER=
|
||||
- OPENVPN_PASSWORD=
|
||||
# Wireguard:
|
||||
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
||||
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||
# Timezone for accurate log times
|
||||
- TZ=
|
||||
# Server list updater
|
||||
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||
- UPDATER_PERIOD=
|
||||
```
|
||||
|
||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||
|
||||
## Fun graphs
|
||||
|
||||
[](https://www.star-history.com/#passteque/gluetun&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/passteque/gluetun/blob/master/LICENSE)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/qdm12/gluetun/ci/internal"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := log.New()
|
||||
if len(os.Args) < 2 {
|
||||
logger.Error("Usage: " + os.Args[0] + " <command>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "mullvad":
|
||||
err = internal.MullvadTest(ctx, logger)
|
||||
case "protonvpn-wireguard-port-forwarding":
|
||||
err = internal.ProtonVPNWireguardPortForwardingTest(ctx, logger)
|
||||
case "protonvpn-openvpn-port-forwarding":
|
||||
err = internal.ProtonVPNOpenVPNPortForwardingTest(ctx, logger)
|
||||
case "private-internet-access-openvpn-port-forwarding":
|
||||
err = internal.PrivateInternetAccessOpenVPNPortForwardingTest(ctx, logger)
|
||||
case "airvpn-wireguard":
|
||||
err = internal.AirVPNWireguardTest(ctx, logger)
|
||||
case "airvpn-openvpn":
|
||||
err = internal.AirVPNOpenVPNTest(ctx, logger)
|
||||
default:
|
||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
||||
}
|
||||
stop()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info("test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module github.com/qdm12/gluetun/ci
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/qdm12/log v0.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
@@ -0,0 +1,54 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func AirVPNWireguardTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"Wireguard private key",
|
||||
"Wireguard preshared key",
|
||||
"Wireguard addresses",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=airvpn",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=United States",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
||||
"WIREGUARD_PRESHARED_KEY=" + secrets[1],
|
||||
"WIREGUARD_ADDRESSES=" + secrets[2],
|
||||
}
|
||||
const timeout = 60 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
|
||||
}
|
||||
|
||||
func AirVPNOpenVPNTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"OpenVPN key",
|
||||
"OpenVPN cert",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=airvpn",
|
||||
"VPN_TYPE=openvpn",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=United States",
|
||||
"OPENVPN_KEY=" + secrets[0],
|
||||
"OPENVPN_CERT=" + secrets[1],
|
||||
}
|
||||
const timeout = 60 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MullvadTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"Wireguard private key",
|
||||
"Wireguard address",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=mullvad",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=USA",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
||||
"WIREGUARD_ADDRESSES=" + secrets[1],
|
||||
}
|
||||
const timeout = 60 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func PrivateInternetAccessOpenVPNPortForwardingTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"OpenVPN username",
|
||||
"OpenVPN password",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=private internet access",
|
||||
"VPN_TYPE=openvpn",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_REGIONS=CA Montreal",
|
||||
"OPENVPN_USER=" + secrets[0],
|
||||
"OPENVPN_PASSWORD=" + secrets[1],
|
||||
"VPN_PORT_FORWARDING=on",
|
||||
}
|
||||
const timeout = 80 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ProtonVPNWireguardPortForwardingTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"Wireguard private key",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=protonvpn",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=United States",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
||||
"VPN_PORT_FORWARDING=on",
|
||||
}
|
||||
const timeout = 80 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
|
||||
}
|
||||
|
||||
func ProtonVPNOpenVPNPortForwardingTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"OpenVPN username",
|
||||
"OpenVPN password",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=protonvpn",
|
||||
"VPN_TYPE=openvpn",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=United States",
|
||||
"OPENVPN_USER=" + secrets[0],
|
||||
"OPENVPN_PASSWORD=" + secrets[1],
|
||||
"VPN_PORT_FORWARDING=on",
|
||||
}
|
||||
const timeout = 80 * time.Second
|
||||
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Info(msg string)
|
||||
Infof(format string, args ...any)
|
||||
}
|
||||
|
||||
func readSecrets(ctx context.Context, expectedSecrets []string,
|
||||
logger Logger,
|
||||
) (lines []string, err error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
lines = make([]string, 0, len(expectedSecrets))
|
||||
|
||||
for i := range expectedSecrets {
|
||||
logger.Infof("🤫 reading %s from Stdin...", expectedSecrets[i])
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
||||
logger.Infof("🤫 %s secret read successfully", expectedSecrets[i])
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
|
||||
}
|
||||
|
||||
if len(lines) < len(expectedSecrets) {
|
||||
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
||||
len(expectedSecrets), len(lines))
|
||||
}
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
var (
|
||||
successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
||||
portForwardingRegexp = regexp.MustCompile(`port forwarded is \d`)
|
||||
)
|
||||
|
||||
func runContainerTest(ctx context.Context, env []string,
|
||||
regexps []*regexp.Regexp, timeout time.Duration, logger Logger,
|
||||
) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Docker client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
config := &container.Config{
|
||||
Image: "qmcgaw/gluetun",
|
||||
StopTimeout: ptrTo(3),
|
||||
Env: env,
|
||||
}
|
||||
hostConfig := &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
||||
}
|
||||
networkConfig := (*network.NetworkingConfig)(nil)
|
||||
platform := (*v1.Platform)(nil)
|
||||
const containerName = "" // auto-generated name
|
||||
|
||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating container: %w", err)
|
||||
}
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Println("Warning during container creation:", warning)
|
||||
}
|
||||
containerID := response.ID
|
||||
defer stopContainer(client, containerID)
|
||||
|
||||
beforeStartTime := time.Now()
|
||||
|
||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting container: %w", err)
|
||||
}
|
||||
|
||||
return waitForLogLines(ctx, client, containerID, beforeStartTime, regexps, logger)
|
||||
}
|
||||
|
||||
func stopContainer(client *client.Client, containerID string) {
|
||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||
defer stopCancel()
|
||||
|
||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("failed to stop container:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForLogLines(ctx context.Context, client *client.Client, containerID string,
|
||||
beforeStartTime time.Time, regexps []*regexp.Regexp, logger Logger,
|
||||
) error {
|
||||
logOptions := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting container logs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
regexpMatched := 0
|
||||
|
||||
var linesSeen []string
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for ctx.Err() == nil {
|
||||
if scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) > 8 { // remove Docker log prefix
|
||||
line = line[8:]
|
||||
}
|
||||
linesSeen = append(linesSeen, line)
|
||||
regex := regexps[regexpMatched]
|
||||
if regex.MatchString(line) {
|
||||
fmt.Println("✅ Expected line logged:", line)
|
||||
if regexpMatched == len(regexps)-1 {
|
||||
return nil
|
||||
}
|
||||
regexpMatched++
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := scanner.Err()
|
||||
if err != nil && err != io.EOF {
|
||||
logSeenLines(linesSeen)
|
||||
return fmt.Errorf("reading log stream: %w", err)
|
||||
}
|
||||
|
||||
// The scanner is either done or cannot read because of EOF
|
||||
logger.Info("the log scanner stopped")
|
||||
logSeenLines(linesSeen)
|
||||
|
||||
// Check if the container is still running
|
||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspecting container: %w", err)
|
||||
}
|
||||
if !inspect.State.Running {
|
||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func logSeenLines(lines []string) {
|
||||
fmt.Println("Logs seen so far:")
|
||||
for _, line := range lines {
|
||||
fmt.Println(" " + line)
|
||||
}
|
||||
}
|
||||
+350
-162
@@ -2,10 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -13,15 +15,17 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/alpine"
|
||||
"github.com/qdm12/gluetun/internal/boringpoll"
|
||||
"github.com/qdm12/gluetun/internal/cli"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/env"
|
||||
"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/mux"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/dns"
|
||||
"github.com/qdm12/gluetun/internal/firewall"
|
||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||
@@ -29,24 +33,28 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
"github.com/qdm12/gluetun/internal/openvpn"
|
||||
"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/routing"
|
||||
"github.com/qdm12/gluetun/internal/server"
|
||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||
"github.com/qdm12/gluetun/internal/socks5"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
"github.com/qdm12/gluetun/internal/tun"
|
||||
"github.com/qdm12/gluetun/internal/updater"
|
||||
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"github.com/qdm12/gluetun/internal/vpn"
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"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/updated/pkg/dnscrypto"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
@@ -64,37 +72,44 @@ func main() {
|
||||
}
|
||||
|
||||
background := context.Background()
|
||||
signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
||||
ctx, cancel := context.WithCancel(background)
|
||||
|
||||
logger := logging.New(logging.Settings{
|
||||
Level: logging.LevelInfo,
|
||||
})
|
||||
logger := log.New(log.SetLevel(log.LevelInfo))
|
||||
|
||||
args := os.Args
|
||||
tun := tun.New()
|
||||
netLinker := netlink.New()
|
||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||
netLinker := netlink.New(netLinkDebugLogger)
|
||||
cli := cli.New()
|
||||
cmder := command.NewCmder()
|
||||
cmder := command.New()
|
||||
|
||||
envReader := env.New(logger)
|
||||
filesReader := files.New()
|
||||
secretsReader := secrets.New()
|
||||
muxReader := mux.New(envReader, filesReader, secretsReader)
|
||||
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, muxReader, tun, netLinker, cmder, cli)
|
||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli)
|
||||
}()
|
||||
|
||||
// Wait for OS signal or run error
|
||||
var err error
|
||||
select {
|
||||
case <-signalCtx.Done():
|
||||
stop()
|
||||
case receivedSignal := <-signalCh:
|
||||
signal.Stop(signalCh)
|
||||
fmt.Println("")
|
||||
logger.Warn("Caught OS signal, shutting down")
|
||||
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||
cancel()
|
||||
case err := <-errorCh:
|
||||
stop()
|
||||
case err = <-errorCh:
|
||||
close(errorCh)
|
||||
if err == nil { // expected exit such as healthcheck
|
||||
os.Exit(0)
|
||||
@@ -103,48 +118,56 @@ func main() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Shutdown timed sequence, and force exit on second OS signal
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case <-errorCh:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
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)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
errCommandUnknown = errors.New("command is unknown")
|
||||
)
|
||||
|
||||
//nolint:gocognit,gocyclo,maintidx
|
||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
args []string, logger logging.ParentLogger, source sources.Source,
|
||||
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
|
||||
cli cli.CLIer) error {
|
||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||
netLinker netLinker, cmder RunStarter,
|
||||
cli clier,
|
||||
) error {
|
||||
if len(args) > 1 { // cli operation
|
||||
switch args[1] {
|
||||
case "healthcheck":
|
||||
return cli.HealthCheck(ctx, source, logger)
|
||||
return cli.HealthCheck(ctx, reader, logger)
|
||||
case "clientkey":
|
||||
return cli.ClientKey(args[2:])
|
||||
case "openvpnconfig":
|
||||
return cli.OpenvpnConfig(logger, source)
|
||||
return cli.OpenvpnConfig(logger, reader, netLinker)
|
||||
case "update":
|
||||
return cli.Update(ctx, args[2:], logger)
|
||||
case "format-servers":
|
||||
return cli.FormatServers(args[2:])
|
||||
case "genkey":
|
||||
return cli.GenKey(args[2:])
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||
return fmt.Errorf("command is unknown: %s", args[1])
|
||||
}
|
||||
}
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
|
||||
defer fmt.Println(gluetunLogo)
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2026-06-30T00:00:00Z")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -154,8 +177,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
Emails: []string{"quentin.mcgaw@gmail.com"},
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
BuildDate: buildInfo.Created,
|
||||
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
|
||||
Created: buildInfo.Created,
|
||||
Announcement: "Your servers data files are now migrated to /gluetun/servers/",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
@@ -165,26 +188,27 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
allSettings, err := source.Read()
|
||||
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 from source
|
||||
// - global log level is parsed below
|
||||
// - firewall Debug and Enabled are booleans parsed from source
|
||||
|
||||
logger.PatchLevel(*allSettings.Log.Level)
|
||||
|
||||
routingLogger := logger.NewChild(logging.Settings{
|
||||
Prefix: "routing: ",
|
||||
})
|
||||
if *allSettings.Firewall.Debug { // To remove in v4
|
||||
routingLogger.PatchLevel(logging.LevelDebug)
|
||||
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"))
|
||||
routingConf := routing.New(netLinker, routingLogger)
|
||||
|
||||
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
|
||||
defaultRoutes, err := routingConf.DefaultRoutes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,68 +218,72 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return err
|
||||
}
|
||||
|
||||
defaultIP, err := routingConf.DefaultIP()
|
||||
iptablesLogLevel, _ := log.ParseLevel(allSettings.Firewall.Iptables.LogLevel)
|
||||
iptablesLogger := logger.New(log.SetComponent("iptables"), log.SetLevel(iptablesLogLevel))
|
||||
|
||||
firewallLogger := logger.New(log.SetComponent("firewall"))
|
||||
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, iptablesLogger, cmder,
|
||||
defaultRoutes, localNetworks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
firewallLogger := logger.NewChild(logging.Settings{
|
||||
Prefix: "firewall: ",
|
||||
})
|
||||
if *allSettings.Firewall.Debug { // To remove in v4
|
||||
firewallLogger.PatchLevel(logging.LevelDebug)
|
||||
}
|
||||
firewallConf := firewall.NewConfig(firewallLogger, cmder,
|
||||
defaultInterface, defaultGateway, localNetworks, defaultIP)
|
||||
if *allSettings.Firewall.Enabled {
|
||||
err = firewallConf.SetEnabled(ctx, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = netLinker.FlushConntrack()
|
||||
if err != nil {
|
||||
logger.Warnf("flushing conntrack failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||
storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "})
|
||||
storage, err := storage.New(storageLogger, constants.ServersData)
|
||||
storageLogger := logger.New(log.SetComponent("storage"))
|
||||
storage, err := storage.New(storageLogger, *allSettings.Storage.ServersEnabled,
|
||||
allSettings.Storage.ServersPath, allSettings.Storage.LegacyServersFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allServers := storage.GetServers()
|
||||
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx,
|
||||
allSettings.IPv6.CheckAddresses, firewallConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
|
||||
ipv6SupportLevel == netlink.IPv6Internet
|
||||
|
||||
err = allSettings.Validate(allServers)
|
||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allSettings.Pprof.HTTPServer.Logger = logger
|
||||
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
|
||||
pprofServer, err := pprof.New(allSettings.Pprof)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create Pprof server: %w", err)
|
||||
return fmt.Errorf("creating Pprof server: %w", err)
|
||||
}
|
||||
|
||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||
|
||||
const clientTimeout = 15 * time.Second
|
||||
const clientTimeout = 35 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
// Create configurators
|
||||
alpineConf := alpine.New()
|
||||
ovpnConf := openvpn.New(
|
||||
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
|
||||
logger.New(log.SetComponent("openvpn configurator")),
|
||||
cmder, puid, pgid)
|
||||
dnsCrypto := dnscrypto.New(httpClient, "", "")
|
||||
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
|
||||
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
|
||||
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
|
||||
ovpnVersion := ovpnConf.Version26
|
||||
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
|
||||
ovpnVersion = ovpnConf.Version25
|
||||
}
|
||||
|
||||
err = printVersions(ctx, logger, []printVersionElement{
|
||||
{name: "Alpine", getVersion: alpineConf.Version},
|
||||
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
|
||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||
{name: "Unbound", getVersion: dnsConf.Version},
|
||||
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
|
||||
return firewall.Version(ctx, cmder)
|
||||
}},
|
||||
{name: "OpenVPN", getVersion: ovpnVersion},
|
||||
{name: "Firewall", getVersion: firewallConf.Version},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -263,40 +291,40 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
logger.Info(allSettings.String())
|
||||
|
||||
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
||||
for _, warning := range allSettings.Warnings() {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
|
||||
const permission = fs.FileMode(0o644)
|
||||
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
||||
err = os.MkdirAll("/gluetun", permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const defaultUsername = "nonrootuser"
|
||||
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create user: %w", err)
|
||||
return fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
if nonRootUsername != defaultUsername {
|
||||
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
||||
}
|
||||
// set it for Unbound
|
||||
// TODO remove this when migrating to qdm12/dns v2
|
||||
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
|
||||
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
||||
|
||||
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := routingConf.Setup(); err != nil {
|
||||
if strings.Contains(err.Error(), "operation not permitted") {
|
||||
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||
}
|
||||
return fmt.Errorf("cannot setup routing: %w", err)
|
||||
return fmt.Errorf("setting up routing: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
logger.Info("routing cleanup...")
|
||||
routingLogger.Info("routing cleanup...")
|
||||
if err := routingConf.TearDown(); err != nil {
|
||||
logger.Error("cannot teardown routing: " + err.Error())
|
||||
routingLogger.Error("cannot teardown routing: " + err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -307,19 +335,17 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return err
|
||||
}
|
||||
|
||||
const tunDevice = "/dev/net/tun"
|
||||
if err := tun.Check(tunDevice); err != nil {
|
||||
logger.Info(err.Error() + "; creating it...")
|
||||
err = tun.Create(tunDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = routingConf.AddLocalRules(localNetworks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding local rules: %w", err)
|
||||
}
|
||||
|
||||
for _, port := range allSettings.Firewall.InputPorts {
|
||||
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, defaultRoute := range defaultRoutes {
|
||||
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} // TODO move inside firewall?
|
||||
|
||||
@@ -334,64 +360,108 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
defaultGroupOptions := []group.Option{
|
||||
group.OptionTimeout(defaultShutdownTimeout),
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess)}
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
}
|
||||
|
||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
|
||||
|
||||
pprofReady := make(chan struct{})
|
||||
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
|
||||
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
|
||||
otherGroupHandler.Add(pprofHandler)
|
||||
<-pprofReady
|
||||
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.NewChild(logging.Settings{Prefix: "port forwarding: "})
|
||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||
httpClient, firewallConf, portForwardLogger)
|
||||
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
|
||||
"port forwarding", goroutine.OptionTimeout(time.Second))
|
||||
go portForwardLooper.Run(portForwardCtx, portForwardDone)
|
||||
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, localNetworksToPrefixes(localNetworks))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DNS loop: %w", err)
|
||||
}
|
||||
|
||||
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
|
||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
||||
unboundLogger)
|
||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||
"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 unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
controlGroupHandler.Add(dnsTickerHandler)
|
||||
|
||||
publicIPLooper := publicip.NewLoop(httpClient,
|
||||
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
||||
allSettings.PublicIP, puid, pgid)
|
||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go publicIPLooper.Run(pubIPCtx, pubIPDone)
|
||||
otherGroupHandler.Add(pubIPHandler)
|
||||
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)
|
||||
}
|
||||
|
||||
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
|
||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
|
||||
tickersGroupHandler.Add(pubIPTickerHandler)
|
||||
socks5Loop := socks5.NewLoop(socks5.Settings{
|
||||
Enabled: *allSettings.Socks5.Enabled,
|
||||
Username: *allSettings.Socks5.Username,
|
||||
Password: *allSettings.Socks5.Password,
|
||||
Address: allSettings.Socks5.ListeningAddress,
|
||||
Logger: logger.New(log.SetComponent("socks5")),
|
||||
})
|
||||
socks5RunError, err := socks5Loop.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting SOCKS5 server loop: %w", err)
|
||||
}
|
||||
|
||||
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
|
||||
allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
|
||||
buildInfo, *allSettings.Version.Enabled)
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
healthChecker := healthcheck.NewChecker(healthLogger)
|
||||
|
||||
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
|
||||
// main DNS local server to make sure no request is blocked by filters.
|
||||
dohDialer, err := doh.New(doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating updater DoH dialer: %w", err)
|
||||
}
|
||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(dohDialer)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||
openvpnFileExtractor, allSettings.Updater)
|
||||
|
||||
boringPollLogger := logger.New(log.SetComponent("boring poll"))
|
||||
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
|
||||
|
||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts,
|
||||
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer,
|
||||
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.NewLooper(allSettings.Updater,
|
||||
allServers, storage, vpnLooper.SetServers, httpClient,
|
||||
logger.NewChild(logging.Settings{Prefix: "updater: "}))
|
||||
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
|
||||
@@ -404,48 +474,66 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
controlGroupHandler.Add(updaterTickerHandler)
|
||||
|
||||
httpProxyLooper := httpproxy.NewLoop(
|
||||
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
|
||||
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.NewLooper(allSettings.Shadowsocks,
|
||||
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
|
||||
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 := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
logger.NewChild(logging.Settings{Prefix: "http server: "}),
|
||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
|
||||
go httpServer.Run(httpServerCtx, httpServerDone)
|
||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6SupportLevel.IsSupported())
|
||||
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.NewChild(logging.Settings{Prefix: "healthcheck: "})
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||
order.OptionTimeout(totalShutdownTimeout),
|
||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
order.OptionOnFailure(defaultShutdownOnFailure))
|
||||
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||
vpnHandler, portForwardHandler, otherGroupHandler)
|
||||
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
|
||||
|
||||
<-ctx.Done()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stoppers := []interface {
|
||||
String() string
|
||||
Stop() error
|
||||
}{
|
||||
portForwardLooper, publicIPLooper, socks5Loop,
|
||||
}
|
||||
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)
|
||||
case err := <-socks5RunError:
|
||||
logger.Errorf("SOCKS5 server loop crashed: %s", err)
|
||||
}
|
||||
|
||||
return orderHandler.Shutdown(context.Background())
|
||||
}
|
||||
@@ -460,7 +548,8 @@ type infoer interface {
|
||||
}
|
||||
|
||||
func printVersions(ctx context.Context, logger infoer,
|
||||
elements []printVersionElement) (err error) {
|
||||
elements []printVersionElement,
|
||||
) (err error) {
|
||||
const timeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -468,10 +557,109 @@ func printVersions(ctx context.Context, logger infoer,
|
||||
for _, element := range elements {
|
||||
version, err := element.getVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting %s version: %w", element.name, err)
|
||||
}
|
||||
logger.Info(element.name + " version: " + version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func localNetworksToPrefixes(localNetworks []routing.LocalNetwork) (prefixes []netip.Prefix) {
|
||||
prefixes = make([]netip.Prefix, len(localNetworks))
|
||||
for i, localNetwork := range localNetworks {
|
||||
prefixes[i] = localNetwork.IPNet
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
type netLinker interface {
|
||||
Addresser
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
FlushConntrack() error
|
||||
PatchLoggerLevel(level log.Level)
|
||||
}
|
||||
|
||||
type Addresser interface {
|
||||
AddrList(linkIndex uint32, family uint8) (
|
||||
addresses []netip.Prefix, err error)
|
||||
AddrReplace(linkIndex uint32, addr netip.Prefix) error
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(family uint8) (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 uint8) (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 uint32) (link netlink.Link, err error)
|
||||
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
|
||||
LinkDel(linkIndex uint32) (err error)
|
||||
LinkSetUp(linkIndex uint32) (err error)
|
||||
LinkSetDown(linkIndex uint32) (err error)
|
||||
LinkSetMTU(linkIndex, mtu uint32) 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 RunStarter interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, err error)
|
||||
RunAndLog(ctx context.Context, commandString string,
|
||||
logger command.Logger) (err error)
|
||||
}
|
||||
|
||||
const gluetunLogo = ` @@@
|
||||
@@@@
|
||||
@@@@@@
|
||||
@@@@.@@ @@@@@@@@@@
|
||||
@@@@.@@@ @@@@@@@@==@@@@
|
||||
@@@.@..@@ @@@@@@@=@..==@@@@
|
||||
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
||||
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
||||
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
||||
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
||||
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
||||
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
||||
@@@@.@--@@@@@@@@@@================@@@@@@@
|
||||
@@@..--@@*@@@@@@================@@@@+*@@
|
||||
@@@.---@@.@@@@=================@@@@--@@
|
||||
@@@-.---@@@@@@================@@@@*--@@@
|
||||
@@@.:-#@@@@@@===============*@@@@.---@@
|
||||
@@@.-------.@@@============@@@@@@.--@@@
|
||||
@@@..--------:@@@=========@@@@@@@@.--@@@
|
||||
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
||||
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
||||
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
||||
@@@@@@=============@@@@@ @@@...------------.*@@@
|
||||
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
||||
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
||||
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
||||
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
||||
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
||||
@@@@@@@@ @@@@ @@@@
|
||||
`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
credentials
|
||||
@@ -0,0 +1,152 @@
|
||||
# devrun
|
||||
|
||||
`devrun` is a small development helper for starting a local `qmcgaw/gluetun` Docker container with provider credentials stored in an encrypted file.
|
||||
|
||||
It solves two practical problems for local development:
|
||||
|
||||
- keeping VPN credentials out of the shell history and out of a plaintext file once setup is complete;
|
||||
- quickly starting a Gluetun container for a specific provider and VPN type with a small set of extra Docker runtime options.
|
||||
|
||||
The tool has four commands:
|
||||
|
||||
- `add-cred`: add or replace credentials for one provider and one VPN type in the encrypted store `credentials`;
|
||||
- `delete-cred`: remove credentials for one provider and one VPN type from the encrypted store `credentials`;
|
||||
- `dump-cred`: print credentials for one provider and one VPN type from the encrypted store `credentials`;
|
||||
- `run`: decrypt credentials on demand, build the required Gluetun environment variables, and run a `qmcgaw/gluetun` container.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go installed locally
|
||||
- Docker installed and a daemon available to the Docker client
|
||||
- an interactive terminal, since the tool prompts for passwords without echoing them
|
||||
|
||||
The Docker client is created from the standard Docker environment, so settings such as `DOCKER_HOST` are honored.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Add credentials
|
||||
|
||||
Add one credential entry to the encrypted store:
|
||||
|
||||
```sh
|
||||
go run ./cmd/main.go add-cred protonvpn openvpn
|
||||
go run ./cmd/main.go add-cred mullvad wireguard
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- if `credentials` does not exist yet, `add-cred` asks for a new credentials password and creates the encrypted store;
|
||||
- if `credentials` already exists, `add-cred` asks for the existing password first, decrypts the store, updates it, and writes it back encrypted;
|
||||
- sensitive fields are read from stdin without echo.
|
||||
|
||||
Prompted values depend on the VPN type:
|
||||
|
||||
- `openvpn`: username and password
|
||||
- `wireguard`: private key, optional address, optional preshared key
|
||||
|
||||
Running `add-cred` again for the same provider and VPN type replaces the existing values for that entry.
|
||||
|
||||
### Delete credentials
|
||||
|
||||
Remove one credential entry from the encrypted store:
|
||||
|
||||
```sh
|
||||
go run ./cmd/main.go delete-cred protonvpn openvpn
|
||||
```
|
||||
|
||||
This asks for the credentials password first, decrypts the store, removes the requested provider and VPN type, and writes the store back encrypted.
|
||||
|
||||
### Dump credentials
|
||||
|
||||
Print one credential entry from the encrypted store:
|
||||
|
||||
```sh
|
||||
go run ./cmd/main.go dump-cred protonvpn openvpn
|
||||
```
|
||||
|
||||
This asks for the credentials password first and then prints the selected provider and VPN type values.
|
||||
|
||||
### Container run
|
||||
|
||||
Run a container using the image `qmcgaw/gluetun` and the encrypted credentials with the `run` command.
|
||||
For example:
|
||||
|
||||
```sh
|
||||
go run ./cmd/main.go run mullvad wireguard
|
||||
go run ./cmd/main.go run protonvpn wireguard -e PORT_FORWARDING=on -p 8000:8000/tcp
|
||||
```
|
||||
|
||||
You will be prompted for the credentials password, the file `credentials` will be decrypted in memory, and the container will be started.
|
||||
|
||||
The following environment variables are always added by the tool:
|
||||
|
||||
- `VPN_SERVICE_PROVIDER=<provider>`
|
||||
- `VPN_TYPE=<vpn-type>`
|
||||
- `LOG_LEVEL=debug`
|
||||
|
||||
The tool also adds `NET_ADMIN` to the container capabilities by default.
|
||||
|
||||
## Credential model
|
||||
|
||||
Internally, the encrypted file stores a binary-encoded map keyed by provider name. Each provider can define `openvpn`, `wireguard`, or both.
|
||||
|
||||
Conceptually, the stored data looks like this:
|
||||
|
||||
- provider `mullvad`: contains `wireguard`
|
||||
- provider `protonvpn`: contains `wireguard`
|
||||
- provider `protonvpn`: contains `openvpn`
|
||||
|
||||
You do not edit this directly. It is stored as encrypted binary data in `credentials`.
|
||||
|
||||
### OpenVPN fields
|
||||
|
||||
- `username` is required;
|
||||
- `password` is required;
|
||||
|
||||
At runtime these map to:
|
||||
|
||||
- `OPENVPN_USER`
|
||||
- `OPENVPN_PASSWORD`
|
||||
|
||||
### WireGuard fields
|
||||
|
||||
- `private_key` is required and must be a valid WireGuard private key;
|
||||
- `address` is optional and must be a valid network prefix if set;
|
||||
- `preshared_key` is optional and must be a valid WireGuard key if set.
|
||||
|
||||
At runtime these map to:
|
||||
|
||||
- `WIREGUARD_PRIVATE_KEY`
|
||||
- `WIREGUARD_ADDRESSES` when `address` is set
|
||||
- `WIREGUARD_PRESHARED_KEY` when `preshared_key` is set
|
||||
|
||||
## Supported extra Docker flags
|
||||
|
||||
The `run` command only accepts a focused subset of Docker-style runtime flags. Unsupported flags return an error.
|
||||
|
||||
Supported flags:
|
||||
|
||||
- `-e`, `--env KEY=VALUE`
|
||||
- `-v`, `--volume SOURCE:TARGET[:mode]`
|
||||
- `-p`, `--publish HOSTPORT:CONTAINERPORT[/proto]`
|
||||
- `--dns IP`
|
||||
- `--device SPEC`
|
||||
- `--label KEY=VALUE`
|
||||
- `--cap-add CAPABILITY`
|
||||
|
||||
## Signals and shutdown
|
||||
|
||||
While the container is running:
|
||||
|
||||
- the first `Ctrl+C` requests a graceful stop with a 5 second timeout;
|
||||
- the second `Ctrl+C` sends a kill signal to the container;
|
||||
- a further interrupt exits the tool immediately.
|
||||
|
||||
## Notes and limitations
|
||||
|
||||
- The container image is fixed to `qmcgaw/gluetun`.
|
||||
- The container name is fixed to `gluetun`.
|
||||
- Credentials are decrypted in memory only during execution.
|
||||
- If the requested provider or VPN type is not present in the encrypted credentials file, the command fails with an explicit error.
|
||||
- The encrypted credential store file is named `credentials`.
|
||||
- This tool is intended for local development convenience, not as a general replacement for `docker run`.
|
||||
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/qdm12/gluetun/devrun/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const minArgs = 2
|
||||
if len(os.Args) < minArgs {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "add-cred":
|
||||
const addCredMinArgs = 4
|
||||
if len(os.Args) < addCredMinArgs {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
`Usage: %s add-cred <provider> <vpn-type>
|
||||
Example: %s add-cred protonvpn wireguard`, os.Args[0], os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
provider := os.Args[2]
|
||||
vpnType := os.Args[3]
|
||||
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
|
||||
return internal.AddCredential(ctx, provider, vpnType)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "add-cred failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "delete-cred":
|
||||
const deleteCredMinArgs = 4
|
||||
if len(os.Args) < deleteCredMinArgs {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
`Usage: %s delete-cred <provider> <vpn-type>
|
||||
Example: %s delete-cred protonvpn openvpn`, os.Args[0], os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
provider := os.Args[2]
|
||||
vpnType := os.Args[3]
|
||||
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
|
||||
return internal.DeleteCredential(ctx, provider, vpnType)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "delete-cred failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "dump-cred":
|
||||
const dumpCredMinArgs = 4
|
||||
if len(os.Args) < dumpCredMinArgs {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
`Usage: %s dump-cred <provider> <vpn-type>
|
||||
Example: %s dump-cred protonvpn wireguard`, os.Args[0], os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
provider := os.Args[2]
|
||||
vpnType := os.Args[3]
|
||||
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
|
||||
return internal.DumpCredential(ctx, provider, vpnType)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "dump-cred failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "run":
|
||||
const runMinArgs = 4
|
||||
if len(os.Args) < runMinArgs {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
`Usage: %s run <provider> <vpn-type> [extra docker flags...]
|
||||
Example: %s run mullvad wireguard -e SERVER_COUNTRIES=USA`, os.Args[0], os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
provider := os.Args[2]
|
||||
vpnType := os.Args[3]
|
||||
extraArgs := os.Args[4:]
|
||||
err := runWithSignals(func(ctx context.Context, forceKill <-chan struct{}) error {
|
||||
return internal.Run(ctx, provider, vpnType, extraArgs, forceKill)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "run failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "unknown command:", os.Args[1])
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintf(os.Stderr, `Usage: %s <command> [args...]
|
||||
|
||||
Commands:
|
||||
add-cred <provider> <vpn-type>
|
||||
Add or replace credentials in the encrypted credentials store.
|
||||
delete-cred <provider> <vpn-type>
|
||||
Delete credentials from the encrypted credentials store.
|
||||
dump-cred <provider> <vpn-type>
|
||||
Print credentials for a provider and VPN type pair.
|
||||
run <provider> <vpn-type> [flags...]
|
||||
Decrypt credentials and run a Gluetun container.
|
||||
Extra flags (e.g. -e PORT_FORWARDING=on) are passed to docker run.`,
|
||||
os.Args[0])
|
||||
}
|
||||
|
||||
func runWithSignals(runFn func(ctx context.Context, forceKill <-chan struct{}) error) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
const signalBufferSize = 3
|
||||
sigCh := make(chan os.Signal, signalBufferSize)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
defer signal.Stop(sigCh)
|
||||
|
||||
forceKill := make(chan struct{})
|
||||
stopSignalLoop := make(chan struct{})
|
||||
signalLoopDone := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(signalLoopDone)
|
||||
|
||||
const secondInterrupt = 2
|
||||
interruptCount := uint(0)
|
||||
forceKillSent := false
|
||||
for {
|
||||
select {
|
||||
case <-stopSignalLoop:
|
||||
return
|
||||
case <-sigCh:
|
||||
interruptCount++
|
||||
switch interruptCount {
|
||||
case 1:
|
||||
cancel()
|
||||
case secondInterrupt:
|
||||
if !forceKillSent {
|
||||
close(forceKill)
|
||||
forceKillSent = true
|
||||
}
|
||||
default:
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := runFn(ctx, forceKill)
|
||||
close(stopSignalLoop)
|
||||
<-signalLoopDone
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module github.com/qdm12/gluetun/devrun
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.7.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
@@ -0,0 +1,251 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
const credentialsFilename = "credentials"
|
||||
|
||||
const (
|
||||
vpnTypeOpenVPN = "openvpn"
|
||||
vpnTypeWireGuard = "wireguard"
|
||||
)
|
||||
|
||||
type providerCredentials struct {
|
||||
OpenVPN *openvpnCredentials
|
||||
WireGuard *wireguardCredentials
|
||||
}
|
||||
|
||||
type openvpnCredentials struct {
|
||||
Username string
|
||||
Password string
|
||||
Key string
|
||||
Cert string
|
||||
}
|
||||
|
||||
type wireguardCredentials struct {
|
||||
PrivateKey string
|
||||
Address string
|
||||
PresharedKey string
|
||||
}
|
||||
|
||||
func loadCredentials(data []byte) (map[string]providerCredentials, error) {
|
||||
credentials := make(map[string]providerCredentials)
|
||||
err := gob.NewDecoder(bytes.NewReader(data)).Decode(&credentials)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding credentials: %w", err)
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func marshalCredentials(credentials map[string]providerCredentials) ([]byte, error) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err := gob.NewEncoder(buffer).Encode(credentials)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encoding credentials: %w", err)
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func validateCredentials(providerNameToCredentials map[string]providerCredentials) error {
|
||||
for provider, credentials := range providerNameToCredentials {
|
||||
if credentials.OpenVPN == nil && credentials.WireGuard == nil {
|
||||
return fmt.Errorf("provider %q has no openvpn or wireguard credentials", provider)
|
||||
}
|
||||
if credentials.OpenVPN != nil {
|
||||
err := validateOpenvpnCredentials(provider, credentials.OpenVPN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if credentials.WireGuard != nil {
|
||||
err := validateWireguardCredentials(provider, credentials.WireGuard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenvpnCredentials(provider string, creds *openvpnCredentials) error {
|
||||
switch {
|
||||
case creds.Username == "" && creds.Password != "":
|
||||
return fmt.Errorf("provider %q openvpn credentials are missing the username", provider)
|
||||
case creds.Password == "" && creds.Username != "":
|
||||
return fmt.Errorf("provider %q openvpn credentials are missing the password", provider)
|
||||
case creds.Username == "" && creds.Password == "" && creds.Key == "" && creds.Cert == "":
|
||||
return fmt.Errorf("provider %q openvpn credentials are missing the username and password", provider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWireguardCredentials(provider string, creds *wireguardCredentials) error {
|
||||
if creds.PrivateKey == "" {
|
||||
return fmt.Errorf("provider %q wireguard credentials are missing the private key", provider)
|
||||
} else if _, err := wgtypes.ParseKey(creds.PrivateKey); err != nil {
|
||||
return fmt.Errorf("provider %q wireguard credentials have an invalid private key: %w", provider, err)
|
||||
}
|
||||
|
||||
if creds.Address != "" {
|
||||
_, err := netip.ParsePrefix(creds.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provider %q wireguard credentials have an invalid address %q: %w", provider, creds.Address, err)
|
||||
}
|
||||
}
|
||||
|
||||
if creds.PresharedKey != "" {
|
||||
if _, err := wgtypes.ParseKey(creds.PresharedKey); err != nil {
|
||||
return fmt.Errorf("provider %q wireguard credentials have an invalid preshared key: %w", provider, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupCredentials(credentials map[string]providerCredentials, provider, vpnType string) ([]string, error) {
|
||||
providerCreds, exists := credentials[provider]
|
||||
if !exists {
|
||||
existing := slices.Collect(maps.Keys(credentials))
|
||||
return nil, fmt.Errorf("no credentials found for provider %q, available providers are: %s",
|
||||
provider, strings.Join(existing, ", "))
|
||||
}
|
||||
|
||||
switch vpnType {
|
||||
case vpnTypeWireGuard:
|
||||
if providerCreds.WireGuard == nil {
|
||||
return nil, fmt.Errorf("no wireguard credentials found for provider %q", provider)
|
||||
}
|
||||
return buildWireGuardEnv(providerCreds.WireGuard), nil
|
||||
case vpnTypeOpenVPN:
|
||||
if providerCreds.OpenVPN == nil {
|
||||
return nil, fmt.Errorf("no openvpn credentials found for provider %q", provider)
|
||||
}
|
||||
return buildOpenvpnEnv(providerCreds.OpenVPN), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
|
||||
}
|
||||
}
|
||||
|
||||
func buildWireGuardEnv(creds *wireguardCredentials) []string {
|
||||
envVars := []string{
|
||||
"WIREGUARD_PRIVATE_KEY=" + creds.PrivateKey,
|
||||
}
|
||||
if creds.Address != "" {
|
||||
envVars = append(envVars, "WIREGUARD_ADDRESSES="+creds.Address)
|
||||
}
|
||||
if creds.PresharedKey != "" {
|
||||
envVars = append(envVars, "WIREGUARD_PRESHARED_KEY="+creds.PresharedKey)
|
||||
}
|
||||
return envVars
|
||||
}
|
||||
|
||||
func buildOpenvpnEnv(creds *openvpnCredentials) []string {
|
||||
return []string{
|
||||
"OPENVPN_USER=" + creds.Username,
|
||||
"OPENVPN_PASSWORD=" + creds.Password,
|
||||
"OPENVPN_KEY=" + creds.Key,
|
||||
"OPENVPN_CERT=" + creds.Cert,
|
||||
}
|
||||
}
|
||||
|
||||
func addCredential(credentials map[string]providerCredentials, provider, vpnType string,
|
||||
openvpnCredentials *openvpnCredentials, wireguardCredentials *wireguardCredentials,
|
||||
) error {
|
||||
providerCredentials := credentials[provider]
|
||||
|
||||
switch vpnType {
|
||||
case vpnTypeOpenVPN:
|
||||
providerCredentials.OpenVPN = openvpnCredentials
|
||||
case vpnTypeWireGuard:
|
||||
providerCredentials.WireGuard = wireguardCredentials
|
||||
default:
|
||||
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
|
||||
}
|
||||
|
||||
credentials[provider] = providerCredentials
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteCredential(credentials map[string]providerCredentials, provider, vpnType string) error {
|
||||
providerCredentials, exists := credentials[provider]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider %q does not exist", provider)
|
||||
}
|
||||
|
||||
switch vpnType {
|
||||
case vpnTypeOpenVPN:
|
||||
if providerCredentials.OpenVPN == nil {
|
||||
return fmt.Errorf("provider %q has no openvpn credentials", provider)
|
||||
}
|
||||
providerCredentials.OpenVPN = nil
|
||||
case vpnTypeWireGuard:
|
||||
if providerCredentials.WireGuard == nil {
|
||||
return fmt.Errorf("provider %q has no wireguard credentials", provider)
|
||||
}
|
||||
providerCredentials.WireGuard = nil
|
||||
default:
|
||||
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
|
||||
}
|
||||
|
||||
if providerCredentials.OpenVPN == nil && providerCredentials.WireGuard == nil {
|
||||
delete(credentials, provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
credentials[provider] = providerCredentials
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatCredentialForDump(provider, vpnType string,
|
||||
providerCredentials providerCredentials,
|
||||
) (output string, err error) {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString("provider: ")
|
||||
builder.WriteString(provider)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("vpn_type: ")
|
||||
builder.WriteString(vpnType)
|
||||
builder.WriteString("\n")
|
||||
|
||||
switch vpnType {
|
||||
case vpnTypeOpenVPN:
|
||||
if providerCredentials.OpenVPN == nil {
|
||||
return "", fmt.Errorf("no openvpn credentials found for provider %q", provider)
|
||||
}
|
||||
builder.WriteString("username: ")
|
||||
builder.WriteString(providerCredentials.OpenVPN.Username)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("password: ")
|
||||
builder.WriteString(providerCredentials.OpenVPN.Password)
|
||||
builder.WriteString("\nkey: ")
|
||||
builder.WriteString(providerCredentials.OpenVPN.Key)
|
||||
builder.WriteString("\ncert: ")
|
||||
builder.WriteString(providerCredentials.OpenVPN.Cert)
|
||||
builder.WriteString("\n")
|
||||
case vpnTypeWireGuard:
|
||||
if providerCredentials.WireGuard == nil {
|
||||
return "", fmt.Errorf("no wireguard credentials found for provider %q", provider)
|
||||
}
|
||||
builder.WriteString("private_key: ")
|
||||
builder.WriteString(providerCredentials.WireGuard.PrivateKey)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("address: ")
|
||||
builder.WriteString(providerCredentials.WireGuard.Address)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("preshared_key: ")
|
||||
builder.WriteString(providerCredentials.WireGuard.PresharedKey)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func Test_addCredential(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialCredentials map[string]providerCredentials
|
||||
provider string
|
||||
vpnType string
|
||||
openvpnCredentials *openvpnCredentials
|
||||
wireguardCreds *wireguardCredentials
|
||||
expectedLength int
|
||||
expectedOpenVPN bool
|
||||
expectedWireGuard bool
|
||||
}{
|
||||
"adds_openvpn_credentials": {
|
||||
initialCredentials: map[string]providerCredentials{},
|
||||
provider: "protonvpn",
|
||||
vpnType: "openvpn",
|
||||
openvpnCredentials: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
expectedLength: 1,
|
||||
expectedOpenVPN: true,
|
||||
},
|
||||
"adds_wireguard_credentials": {
|
||||
initialCredentials: map[string]providerCredentials{},
|
||||
provider: "mullvad",
|
||||
vpnType: "wireguard",
|
||||
wireguardCreds: &wireguardCredentials{
|
||||
PrivateKey: wireguardPrivateKey.String(),
|
||||
Address: "10.0.0.2/32",
|
||||
},
|
||||
expectedLength: 1,
|
||||
expectedWireGuard: true,
|
||||
},
|
||||
"preserves_other_protocol": {
|
||||
initialCredentials: map[string]providerCredentials{
|
||||
"protonvpn": {
|
||||
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
|
||||
},
|
||||
},
|
||||
provider: "protonvpn",
|
||||
vpnType: "openvpn",
|
||||
openvpnCredentials: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
expectedLength: 1,
|
||||
expectedOpenVPN: true,
|
||||
expectedWireGuard: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
credentials := cloneCredentials(testCase.initialCredentials)
|
||||
|
||||
err := addCredential(credentials, testCase.provider, testCase.vpnType,
|
||||
testCase.openvpnCredentials, testCase.wireguardCreds)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
providerCredentials := credentials[testCase.provider]
|
||||
if len(credentials) != testCase.expectedLength {
|
||||
t.Fatalf("expected %d providers, got %d", testCase.expectedLength, len(credentials))
|
||||
}
|
||||
if (providerCredentials.OpenVPN != nil) != testCase.expectedOpenVPN {
|
||||
t.Fatalf("expected openvpn presence %t, got %t", testCase.expectedOpenVPN, providerCredentials.OpenVPN != nil)
|
||||
}
|
||||
if (providerCredentials.WireGuard != nil) != testCase.expectedWireGuard {
|
||||
t.Fatalf("expected wireguard presence %t, got %t", testCase.expectedWireGuard, providerCredentials.WireGuard != nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteCredential(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialCredentials map[string]providerCredentials
|
||||
provider string
|
||||
vpnType string
|
||||
expectedLength int
|
||||
expectedOpenVPN bool
|
||||
expectedWireGuard bool
|
||||
}{
|
||||
"deletes_openvpn_only": {
|
||||
initialCredentials: map[string]providerCredentials{
|
||||
"protonvpn": {
|
||||
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
|
||||
},
|
||||
},
|
||||
provider: "protonvpn",
|
||||
vpnType: "openvpn",
|
||||
expectedLength: 1,
|
||||
expectedWireGuard: true,
|
||||
},
|
||||
"deletes_last_protocol_and_provider": {
|
||||
initialCredentials: map[string]providerCredentials{
|
||||
"protonvpn": {
|
||||
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
},
|
||||
},
|
||||
provider: "protonvpn",
|
||||
vpnType: "openvpn",
|
||||
expectedLength: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
credentials := cloneCredentials(testCase.initialCredentials)
|
||||
|
||||
err := deleteCredential(credentials, testCase.provider, testCase.vpnType)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(credentials) != testCase.expectedLength {
|
||||
t.Fatalf("expected %d providers, got %d", testCase.expectedLength, len(credentials))
|
||||
}
|
||||
|
||||
providerCredentials, exists := credentials[testCase.provider]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
if (providerCredentials.OpenVPN != nil) != testCase.expectedOpenVPN {
|
||||
t.Fatalf("expected openvpn presence %t, got %t", testCase.expectedOpenVPN, providerCredentials.OpenVPN != nil)
|
||||
}
|
||||
if (providerCredentials.WireGuard != nil) != testCase.expectedWireGuard {
|
||||
t.Fatalf("expected wireguard presence %t, got %t", testCase.expectedWireGuard, providerCredentials.WireGuard != nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
credentials map[string]providerCredentials
|
||||
wantError bool
|
||||
}{
|
||||
"both_protocols_valid": {
|
||||
credentials: map[string]providerCredentials{
|
||||
"protonvpn": {
|
||||
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid_wireguard_when_both_present": {
|
||||
credentials: map[string]providerCredentials{
|
||||
"protonvpn": {
|
||||
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
|
||||
WireGuard: &wireguardCredentials{PrivateKey: "invalid"},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateCredentials(testCase.credentials)
|
||||
if testCase.wantError && err == nil {
|
||||
t.Fatal("expected an error but got nil")
|
||||
}
|
||||
if !testCase.wantError && err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_marshalLoadCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
credentials := map[string]providerCredentials{
|
||||
"mullvad": {
|
||||
WireGuard: &wireguardCredentials{
|
||||
PrivateKey: wireguardPrivateKey.String(),
|
||||
Address: "10.0.0.2/32",
|
||||
},
|
||||
},
|
||||
"protonvpn": {
|
||||
OpenVPN: &openvpnCredentials{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded, err := marshalCredentials(credentials)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected marshal error: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := loadCredentials(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
|
||||
if len(decoded) != len(credentials) {
|
||||
t.Fatalf("expected %d providers, got %d", len(credentials), len(decoded))
|
||||
}
|
||||
|
||||
if decoded["mullvad"].WireGuard == nil {
|
||||
t.Fatal("expected mullvad wireguard credentials to be present")
|
||||
}
|
||||
if decoded["protonvpn"].OpenVPN == nil {
|
||||
t.Fatal("expected protonvpn openvpn credentials to be present")
|
||||
}
|
||||
if decoded["protonvpn"].OpenVPN.Password != "pass" {
|
||||
t.Fatalf("expected protonvpn password %q, got %q", "pass", decoded["protonvpn"].OpenVPN.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatCredentialForDump(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
provider string
|
||||
vpnType string
|
||||
providerCredentials providerCredentials
|
||||
expectedOutput string
|
||||
wantError bool
|
||||
}{
|
||||
"openvpn": {
|
||||
provider: "protonvpn",
|
||||
vpnType: vpnTypeOpenVPN,
|
||||
providerCredentials: providerCredentials{
|
||||
OpenVPN: &openvpnCredentials{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
Key: "key",
|
||||
Cert: "cert",
|
||||
},
|
||||
},
|
||||
expectedOutput: "provider: protonvpn\n" +
|
||||
"vpn_type: openvpn\n" +
|
||||
"username: user\n" +
|
||||
"password: pass\n" +
|
||||
"key: key\n" +
|
||||
"cert: cert\n",
|
||||
},
|
||||
"wireguard": {
|
||||
provider: "mullvad",
|
||||
vpnType: vpnTypeWireGuard,
|
||||
providerCredentials: providerCredentials{
|
||||
WireGuard: &wireguardCredentials{
|
||||
PrivateKey: "private",
|
||||
Address: "10.0.0.2/32",
|
||||
PresharedKey: "preshared",
|
||||
},
|
||||
},
|
||||
expectedOutput: "provider: mullvad\n" +
|
||||
"vpn_type: wireguard\n" +
|
||||
"private_key: private\n" +
|
||||
"address: 10.0.0.2/32\n" +
|
||||
"preshared_key: preshared",
|
||||
},
|
||||
"missing_protocol": {
|
||||
provider: "protonvpn",
|
||||
vpnType: vpnTypeOpenVPN,
|
||||
wantError: true,
|
||||
},
|
||||
"unknown_protocol": {
|
||||
provider: "protonvpn",
|
||||
vpnType: "other",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := formatCredentialForDump(
|
||||
testCase.provider,
|
||||
testCase.vpnType,
|
||||
testCase.providerCredentials,
|
||||
)
|
||||
|
||||
if testCase.wantError {
|
||||
if err == nil {
|
||||
t.Fatal("expected an error but got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if output != testCase.expectedOutput {
|
||||
t.Fatalf("expected output %q, got %q", testCase.expectedOutput, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func cloneCredentials(credentials map[string]providerCredentials) map[string]providerCredentials {
|
||||
clone := make(map[string]providerCredentials, len(credentials))
|
||||
for provider, providerCredentials := range credentials {
|
||||
copied := providerCredentials
|
||||
if providerCredentials.OpenVPN != nil {
|
||||
openvpnCredentials := *providerCredentials.OpenVPN
|
||||
copied.OpenVPN = &openvpnCredentials
|
||||
}
|
||||
if providerCredentials.WireGuard != nil {
|
||||
wireguardCredentials := *providerCredentials.WireGuard
|
||||
copied.WireGuard = &wireguardCredentials
|
||||
}
|
||||
clone[provider] = copied
|
||||
}
|
||||
return clone
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// Encryption format: [16-byte salt][12-byte nonce][AES-256-GCM ciphertext+tag]
|
||||
// Key derivation: scrypt(password, salt, N=32768, r=8, p=1, keyLen=32)
|
||||
|
||||
const (
|
||||
saltSize = 16
|
||||
nonceSize = 12
|
||||
keySize = 32
|
||||
scryptN = 32768
|
||||
scryptR = 8
|
||||
scryptP = 1
|
||||
)
|
||||
|
||||
// AddCredential prompts for credential values and stores them in the encrypted credentials file.
|
||||
func AddCredential(ctx context.Context, provider, vpnType string) error {
|
||||
credentials, password, err := loadCredentialsForMutation(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = promptAndAddCredential(ctx, credentials, provider, vpnType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateCredentials(credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating credentials: %w", err)
|
||||
}
|
||||
|
||||
err = writeEncryptedCredentials(credentials, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"Credentials for provider %q and vpn type %q saved to %s\n",
|
||||
provider, vpnType, credentialsFilename,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCredential removes credentials for a provider and VPN type
|
||||
// from the encrypted credentials file.
|
||||
func DeleteCredential(ctx context.Context, provider, vpnType string) error {
|
||||
credentials, password, err := loadExistingCredentialsForMutation(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = deleteCredential(credentials, provider, vpnType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeEncryptedCredentials(credentials, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"Credentials for provider %q and vpn type %q removed from %s\n",
|
||||
provider, vpnType, credentialsFilename,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DumpCredential decrypts the credential store and prints one provider/vpn-type entry.
|
||||
func DumpCredential(ctx context.Context, provider, vpnType string) error {
|
||||
credentials, err := decryptCredentials(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
providerCredentials, exists := credentials[provider]
|
||||
if !exists {
|
||||
existingProviders := slices.Collect(maps.Keys(credentials))
|
||||
return fmt.Errorf("provider %q does not exist, available providers are: %s",
|
||||
provider, strings.Join(existingProviders, ", "))
|
||||
}
|
||||
|
||||
output, err := formatCredentialForDump(provider, vpnType, providerCredentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
// decryptCredentials reads the encrypted credentials file,
|
||||
// prompts for a password, and returns the decrypted credentials.
|
||||
func decryptCredentials(ctx context.Context) (map[string]providerCredentials, error) {
|
||||
password, err := readSecret(ctx, "Enter credentials password: ", false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading password: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := decryptCredentialsFile(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credentials, err := loadCredentials(plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading credentials: %w", err)
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func loadCredentialsForMutation(ctx context.Context) (
|
||||
credentials map[string]providerCredentials,
|
||||
password []byte,
|
||||
err error,
|
||||
) {
|
||||
_, err = os.Stat(credentialsFilename)
|
||||
if os.IsNotExist(err) {
|
||||
password, err = readPasswordConfirmed(ctx,
|
||||
"Enter new credentials password: ",
|
||||
"Confirm new credentials password: ",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading password: %w", err)
|
||||
}
|
||||
return make(map[string]providerCredentials), password, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("stating %s: %w", credentialsFilename, err)
|
||||
}
|
||||
|
||||
password, err = readSecret(ctx, "Enter credentials password: ", false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading password: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := decryptCredentialsFile(password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
credentials, err = loadCredentials(plaintext)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("loading credentials: %w", err)
|
||||
}
|
||||
|
||||
return credentials, password, nil
|
||||
}
|
||||
|
||||
func loadExistingCredentialsForMutation(ctx context.Context) (
|
||||
credentials map[string]providerCredentials,
|
||||
password []byte,
|
||||
err error,
|
||||
) {
|
||||
_, err = os.Stat(credentialsFilename)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil, fmt.Errorf("%s does not exist", credentialsFilename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("stating %s: %w", credentialsFilename, err)
|
||||
}
|
||||
|
||||
password, err = readSecret(ctx, "Enter credentials password: ", false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading password: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := decryptCredentialsFile(password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
credentials, err = loadCredentials(plaintext)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("loading credentials: %w", err)
|
||||
}
|
||||
|
||||
return credentials, password, nil
|
||||
}
|
||||
|
||||
func promptAndAddCredential(
|
||||
ctx context.Context,
|
||||
credentials map[string]providerCredentials,
|
||||
provider, vpnType string,
|
||||
) error {
|
||||
switch vpnType {
|
||||
case vpnTypeOpenVPN:
|
||||
username, err := readLine(ctx, "OpenVPN username: ", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading username: %w", err)
|
||||
}
|
||||
|
||||
password, err := readSecret(ctx, "OpenVPN password: ", username == "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading password: %w", err)
|
||||
}
|
||||
|
||||
key, err := readSecret(ctx, "OpenVPN key: ", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading key: %w", err)
|
||||
}
|
||||
|
||||
cert, err := readSecret(ctx, "OpenVPN cert: ", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading cert: %w", err)
|
||||
}
|
||||
|
||||
openvpnCredentials := &openvpnCredentials{
|
||||
Username: username,
|
||||
Password: string(password),
|
||||
Key: string(key),
|
||||
Cert: string(cert),
|
||||
}
|
||||
err = validateOpenvpnCredentials(provider, openvpnCredentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addCredential(credentials, provider, vpnType, openvpnCredentials, nil)
|
||||
|
||||
case vpnTypeWireGuard:
|
||||
privateKey, err := readSecret(ctx, "WireGuard private key: ", false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading private key: %w", err)
|
||||
}
|
||||
|
||||
address, err := readLine(ctx, "WireGuard address (optional): ", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading address: %w", err)
|
||||
}
|
||||
|
||||
presharedKey, err := readSecret(
|
||||
ctx,
|
||||
"WireGuard preshared key (optional): ",
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading preshared key: %w", err)
|
||||
}
|
||||
|
||||
wireguardCredentials := &wireguardCredentials{
|
||||
PrivateKey: string(privateKey),
|
||||
Address: address,
|
||||
PresharedKey: string(presharedKey),
|
||||
}
|
||||
err = validateWireguardCredentials(provider, wireguardCredentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addCredential(credentials, provider, vpnType, nil, wireguardCredentials)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
|
||||
}
|
||||
}
|
||||
|
||||
func writeEncryptedCredentials(
|
||||
credentials map[string]providerCredentials,
|
||||
password []byte,
|
||||
) error {
|
||||
plaintext, err := marshalCredentials(credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding credentials: %w", err)
|
||||
}
|
||||
|
||||
encrypted, err := encryptData(plaintext, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypting credentials: %w", err)
|
||||
}
|
||||
|
||||
const filePerms = 0o600
|
||||
err = os.WriteFile(credentialsFilename, encrypted, filePerms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing %s: %w", credentialsFilename, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptCredentialsFile(password []byte) ([]byte, error) {
|
||||
encryptedData, err := os.ReadFile(credentialsFilename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", credentialsFilename, err)
|
||||
}
|
||||
|
||||
plaintext, err := decryptData(encryptedData, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypting credentials: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func readSecret(ctx context.Context, prompt string, allowEmpty bool) ([]byte, error) {
|
||||
fmt.Print(prompt)
|
||||
|
||||
passwordFD, err := syscall.Dup(syscall.Stdin)
|
||||
if err != nil {
|
||||
fmt.Println()
|
||||
return nil, fmt.Errorf("duplicating stdin file descriptor: %w", err)
|
||||
}
|
||||
|
||||
var closeFDOnce sync.Once
|
||||
closePasswordFD := func() {
|
||||
closeFDOnce.Do(func() {
|
||||
_ = syscall.Close(passwordFD)
|
||||
})
|
||||
}
|
||||
|
||||
passwordResult := make(chan struct {
|
||||
password []byte
|
||||
err error
|
||||
})
|
||||
|
||||
go func() {
|
||||
password, err := term.ReadPassword(passwordFD)
|
||||
closePasswordFD()
|
||||
result := struct {
|
||||
password []byte
|
||||
err error
|
||||
}{
|
||||
password: password,
|
||||
err: err,
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case passwordResult <- result:
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
closePasswordFD()
|
||||
fmt.Println()
|
||||
return nil, ctx.Err()
|
||||
case result := <-passwordResult:
|
||||
closePasswordFD()
|
||||
fmt.Println()
|
||||
if result.err != nil {
|
||||
return nil, fmt.Errorf("reading hidden input from terminal: %w", result.err)
|
||||
}
|
||||
if len(result.password) == 0 && !allowEmpty {
|
||||
return nil, fmt.Errorf("value cannot be empty")
|
||||
}
|
||||
return result.password, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readLine(ctx context.Context, prompt string, allowEmpty bool) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
|
||||
inputFD, err := syscall.Dup(syscall.Stdin)
|
||||
if err != nil {
|
||||
fmt.Println()
|
||||
return "", fmt.Errorf("duplicating stdin file descriptor: %w", err)
|
||||
}
|
||||
|
||||
var closeFDOnce sync.Once
|
||||
closeInputFD := func() {
|
||||
closeFDOnce.Do(func() {
|
||||
_ = syscall.Close(inputFD)
|
||||
})
|
||||
}
|
||||
|
||||
inputResult := make(chan struct {
|
||||
value string
|
||||
err error
|
||||
})
|
||||
|
||||
go func() {
|
||||
inputFile := os.NewFile(uintptr(inputFD), "stdin")
|
||||
reader := bufio.NewReader(inputFile)
|
||||
value, err := reader.ReadString('\n')
|
||||
closeInputFD()
|
||||
value = strings.TrimRight(value, "\r\n")
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
|
||||
result := struct {
|
||||
value string
|
||||
err error
|
||||
}{
|
||||
value: value,
|
||||
err: err,
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case inputResult <- result:
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
closeInputFD()
|
||||
fmt.Println()
|
||||
return "", ctx.Err()
|
||||
case result := <-inputResult:
|
||||
closeInputFD()
|
||||
if result.err != nil {
|
||||
return "", fmt.Errorf("reading line from terminal: %w", result.err)
|
||||
}
|
||||
if result.value == "" && !allowEmpty {
|
||||
return "", fmt.Errorf("value cannot be empty")
|
||||
}
|
||||
return result.value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readPasswordConfirmed(
|
||||
ctx context.Context,
|
||||
prompt, confirmationPrompt string,
|
||||
) ([]byte, error) {
|
||||
password, err := readSecret(ctx, prompt, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
confirmation, err := readSecret(ctx, confirmationPrompt, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(password) != string(confirmation) {
|
||||
return nil, fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func deriveKey(password, salt []byte) ([]byte, error) {
|
||||
key, err := scrypt.Key(password, salt, scryptN, scryptR, scryptP, keySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deriving key with scrypt: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func encryptData(plaintext, password []byte) ([]byte, error) {
|
||||
salt := make([]byte, saltSize)
|
||||
_, err := io.ReadFull(rand.Reader, salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating salt: %w", err)
|
||||
}
|
||||
|
||||
key, err := deriveKey(password, salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating AES cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, nonceSize)
|
||||
_, err = io.ReadFull(rand.Reader, nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||
|
||||
result := make([]byte, 0, saltSize+nonceSize+len(ciphertext))
|
||||
result = append(result, salt...)
|
||||
result = append(result, nonce...)
|
||||
result = append(result, ciphertext...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func decryptData(data, password []byte) ([]byte, error) {
|
||||
const minSize = saltSize + nonceSize + 16 // 16 is the GCM tag size
|
||||
if len(data) < minSize {
|
||||
return nil, fmt.Errorf("encrypted data too short: %d bytes", len(data))
|
||||
}
|
||||
|
||||
salt := data[:saltSize]
|
||||
nonce := data[saltSize : saltSize+nonceSize]
|
||||
ciphertext := data[saltSize+nonceSize:]
|
||||
|
||||
key, err := deriveKey(password, salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating AES cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating GCM: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypting data (wrong password?): %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type containerOptions struct {
|
||||
env []string
|
||||
binds []string
|
||||
ports nat.PortMap
|
||||
dns []string
|
||||
devices []container.DeviceMapping
|
||||
labels map[string]string
|
||||
capAdd []string
|
||||
}
|
||||
|
||||
// Run decrypts credentials, builds the container environment, and runs a Gluetun container.
|
||||
// extraArgs is the list of additional flags (e.g. ["-e", "PORT_FORWARDING=on", "-v", "/host:/container"]).
|
||||
func Run(ctx context.Context, provider, vpnType string, extraArgs []string,
|
||||
forceKill <-chan struct{},
|
||||
) error {
|
||||
credentials, err := decryptCredentials(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading credentials: %w", err)
|
||||
}
|
||||
|
||||
credentialEnvVars, err := lookupCredentials(credentials, provider, vpnType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extraOpts, err := parseExtraArgs(extraArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing extra flags: %w", err)
|
||||
}
|
||||
opts := extraOpts
|
||||
opts.env = append(opts.env,
|
||||
"VPN_SERVICE_PROVIDER="+provider,
|
||||
"VPN_TYPE="+vpnType,
|
||||
"LOG_LEVEL=debug",
|
||||
)
|
||||
opts.env = append(opts.env, credentialEnvVars...)
|
||||
opts.capAdd = append(opts.capAdd, "NET_ADMIN")
|
||||
|
||||
return runContainer(ctx, opts, forceKill)
|
||||
}
|
||||
|
||||
// parseExtraArgs parses extra arguments and maps them to container options.
|
||||
// Supported flags:
|
||||
//
|
||||
// -e, --env KEY=VALUE - environment variable
|
||||
// -v, --volume SPEC - volume mount (e.g., "/host:/container" or "name:/container")
|
||||
// -p, --publish PORT:PORT - port mapping
|
||||
// --dns IP - DNS server
|
||||
// --device SPEC - device access (e.g., "/dev/net/tun")
|
||||
// --label KEY=VALUE - container label
|
||||
// --cap-add CAPABILITY - add Linux capability (e.g., "SYS_PTRACE")
|
||||
func parseExtraArgs(args []string) (opts containerOptions, err error) { //nolint:gocognit,gocyclo
|
||||
opts = containerOptions{
|
||||
ports: make(nat.PortMap),
|
||||
labels: make(map[string]string),
|
||||
}
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "-e" || arg == "--env":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
opts.env = append(opts.env, args[i])
|
||||
case strings.HasPrefix(arg, "-e="):
|
||||
opts.env = append(opts.env, strings.TrimPrefix(arg, "-e="))
|
||||
case strings.HasPrefix(arg, "--env="):
|
||||
opts.env = append(opts.env, strings.TrimPrefix(arg, "--env="))
|
||||
|
||||
case arg == "-v" || arg == "--volume":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
opts.binds = append(opts.binds, args[i])
|
||||
case strings.HasPrefix(arg, "-v="):
|
||||
opts.binds = append(opts.binds, strings.TrimPrefix(arg, "-v="))
|
||||
case strings.HasPrefix(arg, "--volume="):
|
||||
opts.binds = append(opts.binds, strings.TrimPrefix(arg, "--volume="))
|
||||
|
||||
case arg == "-p" || arg == "--publish":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
if err := parsePortMapping(opts.ports, args[i]); err != nil {
|
||||
return opts, fmt.Errorf("parsing port mapping: %w", err)
|
||||
}
|
||||
case strings.HasPrefix(arg, "-p="):
|
||||
if err := parsePortMapping(opts.ports, strings.TrimPrefix(arg, "-p=")); err != nil {
|
||||
return opts, fmt.Errorf("parsing port mapping: %w", err)
|
||||
}
|
||||
case strings.HasPrefix(arg, "--publish="):
|
||||
if err := parsePortMapping(opts.ports, strings.TrimPrefix(arg, "--publish=")); err != nil {
|
||||
return opts, fmt.Errorf("parsing port mapping: %w", err)
|
||||
}
|
||||
|
||||
case arg == "--dns":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
opts.dns = append(opts.dns, args[i])
|
||||
case strings.HasPrefix(arg, "--dns="):
|
||||
opts.dns = append(opts.dns, strings.TrimPrefix(arg, "--dns="))
|
||||
|
||||
case arg == "--device":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
parseDeviceMapping(&opts.devices, args[i])
|
||||
case strings.HasPrefix(arg, "--device="):
|
||||
parseDeviceMapping(&opts.devices, strings.TrimPrefix(arg, "--device="))
|
||||
|
||||
case arg == "--label":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
parseLabel(opts.labels, args[i])
|
||||
case strings.HasPrefix(arg, "--label="):
|
||||
parseLabel(opts.labels, strings.TrimPrefix(arg, "--label="))
|
||||
|
||||
case arg == "--cap-add":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("flag %q requires an argument", arg)
|
||||
}
|
||||
i++
|
||||
opts.capAdd = append(opts.capAdd, args[i])
|
||||
case strings.HasPrefix(arg, "--cap-add="):
|
||||
opts.capAdd = append(opts.capAdd, strings.TrimPrefix(arg, "--cap-add="))
|
||||
|
||||
default:
|
||||
return opts, fmt.Errorf("unsupported flag %q", arg)
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func parsePortMapping(portMap nat.PortMap, spec string) error {
|
||||
port, bindings, err := nat.ParsePortSpecs([]string{spec})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for p, binding := range bindings {
|
||||
portMap[p] = binding
|
||||
}
|
||||
for p := range port {
|
||||
if _, exists := portMap[p]; !exists {
|
||||
portMap[p] = []nat.PortBinding{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDeviceMapping(devices *[]container.DeviceMapping, spec string) {
|
||||
parts := strings.SplitN(spec, ":", 3) //nolint:mnd
|
||||
pathOnHost := parts[0]
|
||||
pathInContainer := pathOnHost
|
||||
permissions := "rwm"
|
||||
|
||||
if len(parts) >= 2 { //nolint:mnd
|
||||
pathInContainer = parts[1]
|
||||
}
|
||||
if len(parts) >= 3 { //nolint:mnd
|
||||
permissions = parts[2]
|
||||
}
|
||||
|
||||
*devices = append(*devices, container.DeviceMapping{
|
||||
PathOnHost: pathOnHost,
|
||||
PathInContainer: pathInContainer,
|
||||
CgroupPermissions: permissions,
|
||||
})
|
||||
}
|
||||
|
||||
func parseLabel(labels map[string]string, kv string) {
|
||||
parts := strings.SplitN(kv, "=", 2) //nolint:mnd
|
||||
key := parts[0]
|
||||
value := ""
|
||||
if len(parts) > 1 {
|
||||
value = parts[1]
|
||||
}
|
||||
labels[key] = value
|
||||
}
|
||||
|
||||
func runContainer(ctx context.Context, opts containerOptions, forceKill <-chan struct{}) error {
|
||||
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating docker client: %w", err)
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
hasTTY := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
|
||||
containerConfig := &container.Config{
|
||||
Image: "qmcgaw/gluetun",
|
||||
Env: opts.env,
|
||||
Labels: opts.labels,
|
||||
Tty: hasTTY,
|
||||
}
|
||||
|
||||
mounts := make([]mount.Mount, 0, len(opts.binds))
|
||||
for _, bind := range opts.binds {
|
||||
m, err := parseBindMount(bind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing bind mount %q: %w", bind, err)
|
||||
}
|
||||
mounts = append(mounts, m)
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
CapAdd: opts.capAdd,
|
||||
Binds: opts.binds,
|
||||
Mounts: mounts,
|
||||
PortBindings: opts.ports,
|
||||
DNS: opts.dns,
|
||||
}
|
||||
hostConfig.Devices = opts.devices
|
||||
|
||||
networkConfig := &network.NetworkingConfig{}
|
||||
|
||||
platform := (*v1.Platform)(nil)
|
||||
|
||||
const containerName = "gluetun"
|
||||
response, err := dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, platform, containerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating container: %w", err)
|
||||
}
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintln(os.Stderr, "container creation warning:", warning)
|
||||
}
|
||||
containerID := response.ID
|
||||
|
||||
err = dockerClient.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting container: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Container started (id: %.12s)\n", containerID)
|
||||
|
||||
streamLogsErr := make(chan error, 1)
|
||||
go func() {
|
||||
streamLogsErr <- streamLogs(context.Background(), dockerClient, containerID, hasTTY)
|
||||
}()
|
||||
|
||||
contextDone := ctx.Done()
|
||||
forceKillSignal := forceKill
|
||||
for {
|
||||
select {
|
||||
case err := <-streamLogsErr:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-contextDone:
|
||||
fmt.Fprintln(os.Stderr, "\nReceived interrupt, stopping container (5s timeout)...")
|
||||
err = stopContainer(dockerClient, containerID)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "stopping container:", err)
|
||||
}
|
||||
contextDone = nil
|
||||
case <-forceKillSignal:
|
||||
fmt.Fprintln(os.Stderr, "\nReceived second interrupt, killing container...")
|
||||
err = killContainer(dockerClient, containerID)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "killing container:", err)
|
||||
}
|
||||
forceKillSignal = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseBindMount(bind string) (mount.Mount, error) {
|
||||
parts := strings.SplitN(bind, ":", 3) //nolint:mnd
|
||||
if len(parts) < 2 { //nolint:mnd
|
||||
return mount.Mount{}, fmt.Errorf("invalid bind mount format: %q (expected source:target[:mode])", bind)
|
||||
}
|
||||
|
||||
source := parts[0]
|
||||
target := parts[1]
|
||||
readOnly := len(parts) > 2 && strings.Contains(parts[2], "ro") //nolint:mnd
|
||||
|
||||
return mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: source,
|
||||
Target: target,
|
||||
ReadOnly: readOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func stopContainer(dockerClient *client.Client, containerID string) error {
|
||||
const stopTimeout = 5 * time.Second
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||
defer stopCancel()
|
||||
timeoutSeconds := int(stopTimeout.Seconds())
|
||||
return dockerClient.ContainerStop(stopCtx, containerID, container.StopOptions{Timeout: &timeoutSeconds})
|
||||
}
|
||||
|
||||
func killContainer(dockerClient *client.Client, containerID string) error {
|
||||
return dockerClient.ContainerKill(context.Background(), containerID, "KILL")
|
||||
}
|
||||
|
||||
func streamLogs(ctx context.Context, dockerClient *client.Client, containerID string, hasTTY bool) error {
|
||||
logOptions := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Timestamps: false,
|
||||
}
|
||||
|
||||
reader, err := dockerClient.ContainerLogs(ctx, containerID, logOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting container logs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if hasTTY {
|
||||
_, err = io.Copy(os.Stdout, reader)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("streaming container logs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +1,69 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.17
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.2
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.18
|
||||
github.com/breml/rootcerts v0.3.4
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/qdm12/dns v1.11.0
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/jsimonetti/rtnetlink v1.4.2
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/mdlayher/genetlink v1.4.0
|
||||
github.com/mdlayher/netlink v1.11.2
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||
github.com/qdm12/gluetun-servers v0.1.0
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.1.0
|
||||
github.com/qdm12/gotree v0.2.0
|
||||
github.com/qdm12/govalid v0.1.0
|
||||
github.com/qdm12/ss-server v0.4.0
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
|
||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
||||
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.11.1
|
||||
github.com/ti-mo/netfilter v0.5.3
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.38.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.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/uuid v1.2.0 // indirect
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mdlayher/genetlink v1.0.0 // indirect
|
||||
github.com/mdlayher/netlink v1.4.0 // indirect
|
||||
github.com/miekg/dns v1.1.40 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/socket v0.6.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/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/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/tools v0.45.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
|
||||
)
|
||||
|
||||
@@ -1,257 +1,205 @@
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/breml/rootcerts v0.2.2 h1:hkHEpbTdYaNvDoYeq+mwRvCeg/YTTl23DjQ1Tnj71Zs=
|
||||
github.com/breml/rootcerts v0.2.2/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.18 h1:pUn7/P8qdGmHd6JmE3bCQXPblZs3vruWR98nLODQLJg=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.18/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A=
|
||||
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.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
|
||||
github.com/breml/rootcerts v0.3.4/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/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q=
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
||||
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
|
||||
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
||||
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
||||
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
|
||||
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
|
||||
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
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-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY=
|
||||
github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
|
||||
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
|
||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
||||
github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
|
||||
github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
|
||||
github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
|
||||
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c h1:l8qz53IqEXRGK0X62gWwipG077Fz5eNM7qe4mUbAr/Q=
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c/go.mod h1:vgRg8Skq9+RNp1THecwMI7SGsnIwO/NPMfYenNTgpAc=
|
||||
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/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
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/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=
|
||||
github.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
|
||||
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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
|
||||
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Package alpine defines a configurator to interact with the Alpine operating system.
|
||||
package alpine
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
)
|
||||
|
||||
var _ Alpiner = (*Alpine)(nil)
|
||||
|
||||
type Alpiner interface {
|
||||
UserCreater
|
||||
VersionGetter
|
||||
}
|
||||
|
||||
type Alpine struct {
|
||||
alpineReleasePath string
|
||||
passwdPath string
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package alpine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
)
|
||||
|
||||
type UserCreater interface {
|
||||
CreateUser(username string, uid int) (createdUsername string, err error)
|
||||
}
|
||||
|
||||
// CreateUser creates a user in Alpine with the given UID.
|
||||
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||
UIDStr := strconv.Itoa(uid)
|
||||
@@ -39,11 +31,12 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
return "", fmt.Errorf("%w: with name %s for ID %s instead of %d",
|
||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||
return "", fmt.Errorf("user already exists: with name %s for ID %s instead of %d",
|
||||
username, u.Uid, uid)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type VersionGetter interface {
|
||||
Version(ctx context.Context) (version string, err error)
|
||||
}
|
||||
|
||||
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package amneziawg
|
||||
|
||||
type Amneziawg struct {
|
||||
logger Logger
|
||||
settings Settings
|
||||
netlink NetLinker
|
||||
}
|
||||
|
||||
func New(settings Settings, netlink NetLinker,
|
||||
logger Logger,
|
||||
) (a *Amneziawg, err error) {
|
||||
settings.SetDefaults()
|
||||
if err := settings.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Amneziawg{
|
||||
logger: logger,
|
||||
settings: settings,
|
||||
netlink: netlink,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const validKeyString = "oMNSf/zJ0pt1ciy+qIRk8Rlyfs9accwuRLnKd85Yl1Q="
|
||||
logger := NewMockLogger(nil)
|
||||
netLinker := NewMockNetLinker(nil)
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
amneziawg *Amneziawg
|
||||
err error
|
||||
}{
|
||||
"bad_settings": {
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
PrivateKey: "",
|
||||
},
|
||||
},
|
||||
err: errors.New("private key is missing"),
|
||||
},
|
||||
"minimal valid settings": {
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
PrivateKey: validKeyString,
|
||||
PublicKey: validKeyString,
|
||||
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 0),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
|
||||
},
|
||||
FirewallMark: 100,
|
||||
},
|
||||
},
|
||||
amneziawg: &Amneziawg{
|
||||
logger: logger,
|
||||
netlink: netLinker,
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
InterfaceName: "wg0",
|
||||
PrivateKey: validKeyString,
|
||||
PublicKey: validKeyString,
|
||||
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 51820),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
|
||||
},
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
FirewallMark: 100,
|
||||
MTU: device.DefaultMTU,
|
||||
IPv6: ptrTo(false),
|
||||
Implementation: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguard, err := New(testCase.settings, netLinker, logger)
|
||||
|
||||
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.amneziawg, wireguard)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package amneziawg
|
||||
|
||||
func ptrTo[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package amneziawg
|
||||
|
||||
//go:generate mockgen -destination=log_mock_test.go -package amneziawg . Logger
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: Logger)
|
||||
|
||||
// Package amneziawg is a generated GoMock package.
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debug mocks base method.
|
||||
func (m *MockLogger) Debug(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Debug", arg0)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug.
|
||||
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||
}
|
||||
|
||||
// Debugf mocks base method.
|
||||
func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Debugf", varargs...)
|
||||
}
|
||||
|
||||
// Debugf indicates an expected call of Debugf.
|
||||
func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
|
||||
// Errorf mocks base method.
|
||||
func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Errorf", varargs...)
|
||||
}
|
||||
|
||||
// Errorf indicates an expected call of Errorf.
|
||||
func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info.
|
||||
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=netlinker_mock_test.go -package amneziawg . NetLinker
|
||||
|
||||
type NetLinker interface {
|
||||
AddrReplace(linkIndex uint32, addr netip.Prefix) error
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(family uint8) (routes []netlink.Route, err error)
|
||||
RouteAdd(route netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleAdd(rule netlink.Rule) error
|
||||
RuleDel(rule netlink.Rule) error
|
||||
}
|
||||
|
||||
type Linker interface {
|
||||
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkSetUp(linkIndex uint32) error
|
||||
LinkSetDown(linkIndex uint32) error
|
||||
LinkDel(linkIndex uint32) error
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: NetLinker)
|
||||
|
||||
// Package amneziawg is a generated GoMock package.
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
netip "net/netip"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
netlink "github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
|
||||
// MockNetLinker is a mock of NetLinker interface.
|
||||
type MockNetLinker struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNetLinkerMockRecorder
|
||||
}
|
||||
|
||||
// MockNetLinkerMockRecorder is the mock recorder for MockNetLinker.
|
||||
type MockNetLinkerMockRecorder struct {
|
||||
mock *MockNetLinker
|
||||
}
|
||||
|
||||
// NewMockNetLinker creates a new mock instance.
|
||||
func NewMockNetLinker(ctrl *gomock.Controller) *MockNetLinker {
|
||||
mock := &MockNetLinker{ctrl: ctrl}
|
||||
mock.recorder = &MockNetLinkerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockNetLinker) EXPECT() *MockNetLinkerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddrReplace mocks base method.
|
||||
func (m *MockNetLinker) AddrReplace(arg0 uint32, arg1 netip.Prefix) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddrReplace", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddrReplace indicates an expected call of AddrReplace.
|
||||
func (mr *MockNetLinkerMockRecorder) AddrReplace(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrReplace", reflect.TypeOf((*MockNetLinker)(nil).AddrReplace), arg0, arg1)
|
||||
}
|
||||
|
||||
// IsWireguardSupported mocks base method.
|
||||
func (m *MockNetLinker) IsWireguardSupported() (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsWireguardSupported")
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsWireguardSupported indicates an expected call of IsWireguardSupported.
|
||||
func (mr *MockNetLinkerMockRecorder) IsWireguardSupported() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWireguardSupported", reflect.TypeOf((*MockNetLinker)(nil).IsWireguardSupported))
|
||||
}
|
||||
|
||||
// LinkAdd mocks base method.
|
||||
func (m *MockNetLinker) LinkAdd(arg0 netlink.Link) (uint32, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkAdd", arg0)
|
||||
ret0, _ := ret[0].(uint32)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkAdd indicates an expected call of LinkAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkAdd", reflect.TypeOf((*MockNetLinker)(nil).LinkAdd), arg0)
|
||||
}
|
||||
|
||||
// LinkByName mocks base method.
|
||||
func (m *MockNetLinker) LinkByName(arg0 string) (netlink.Link, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkByName", arg0)
|
||||
ret0, _ := ret[0].(netlink.Link)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkByName indicates an expected call of LinkByName.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkByName(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkByName", reflect.TypeOf((*MockNetLinker)(nil).LinkByName), arg0)
|
||||
}
|
||||
|
||||
// LinkDel mocks base method.
|
||||
func (m *MockNetLinker) LinkDel(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkDel", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkDel indicates an expected call of LinkDel.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkDel(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkDel", reflect.TypeOf((*MockNetLinker)(nil).LinkDel), arg0)
|
||||
}
|
||||
|
||||
// LinkList mocks base method.
|
||||
func (m *MockNetLinker) LinkList() ([]netlink.Link, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkList")
|
||||
ret0, _ := ret[0].([]netlink.Link)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkList indicates an expected call of LinkList.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkList() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkList", reflect.TypeOf((*MockNetLinker)(nil).LinkList))
|
||||
}
|
||||
|
||||
// LinkSetDown mocks base method.
|
||||
func (m *MockNetLinker) LinkSetDown(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkSetDown", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkSetDown indicates an expected call of LinkSetDown.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkSetDown(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetDown", reflect.TypeOf((*MockNetLinker)(nil).LinkSetDown), arg0)
|
||||
}
|
||||
|
||||
// LinkSetUp mocks base method.
|
||||
func (m *MockNetLinker) LinkSetUp(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkSetUp", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkSetUp indicates an expected call of LinkSetUp.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkSetUp(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetUp", reflect.TypeOf((*MockNetLinker)(nil).LinkSetUp), arg0)
|
||||
}
|
||||
|
||||
// RouteAdd mocks base method.
|
||||
func (m *MockNetLinker) RouteAdd(arg0 netlink.Route) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RouteAdd", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RouteAdd indicates an expected call of RouteAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) RouteAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteAdd", reflect.TypeOf((*MockNetLinker)(nil).RouteAdd), arg0)
|
||||
}
|
||||
|
||||
// RouteList mocks base method.
|
||||
func (m *MockNetLinker) RouteList(arg0 byte) ([]netlink.Route, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RouteList", arg0)
|
||||
ret0, _ := ret[0].([]netlink.Route)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RouteList indicates an expected call of RouteList.
|
||||
func (mr *MockNetLinkerMockRecorder) RouteList(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteList", reflect.TypeOf((*MockNetLinker)(nil).RouteList), arg0)
|
||||
}
|
||||
|
||||
// RuleAdd mocks base method.
|
||||
func (m *MockNetLinker) RuleAdd(arg0 netlink.Rule) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RuleAdd", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RuleAdd indicates an expected call of RuleAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) RuleAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleAdd", reflect.TypeOf((*MockNetLinker)(nil).RuleAdd), arg0)
|
||||
}
|
||||
|
||||
// RuleDel mocks base method.
|
||||
func (m *MockNetLinker) RuleDel(arg0 netlink.Rule) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RuleDel", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RuleDel indicates an expected call of RuleDel.
|
||||
func (mr *MockNetLinkerMockRecorder) RuleDel(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleDel", reflect.TypeOf((*MockNetLinker)(nil).RuleDel), arg0)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
amneziaconn "github.com/amnezia-vpn/amneziawg-go/conn"
|
||||
amneziadevice "github.com/amnezia-vpn/amneziawg-go/device"
|
||||
amneziatun "github.com/amnezia-vpn/amneziawg-go/tun"
|
||||
"github.com/qdm12/gluetun/internal/cleanup"
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
)
|
||||
|
||||
// Run runs the amneziawg interface and waits until the context is done, then it cleans up the
|
||||
// interface and returns any error that occurred during setup or waiting. It sends an error to
|
||||
// waitError if any error occurs during setup or waiting, otherwise it sends nil when the context
|
||||
// is done. It sends a signal to ready when the setup is complete and the interface is ready to use.
|
||||
// See https://github.com/amnezia-vpn/amneziawg-go/blob/master/main.go
|
||||
func (a *Amneziawg) Run(ctx context.Context, waitError chan<- error, ready chan<- struct{}) {
|
||||
setup := func(ctx context.Context, cleanups *cleanup.Cleanups) (
|
||||
linkIndex uint32, waitAndCleanup func() error, err error,
|
||||
) {
|
||||
return setupUserspace(ctx, a.settings.Wireguard.InterfaceName,
|
||||
a.netlink, a.settings.Wireguard.MTU, cleanups, a.logger, a.settings)
|
||||
}
|
||||
|
||||
wireguard.Run(ctx, waitError, ready, setup, a.settings.Wireguard, a.netlink, a.logger)
|
||||
}
|
||||
|
||||
func setupUserspace(ctx context.Context,
|
||||
interfaceName string, netLinker NetLinker, mtu uint32,
|
||||
cleanups *cleanup.Cleanups, logger Logger,
|
||||
settings Settings,
|
||||
) (
|
||||
linkIndex uint32, waitAndCleanup func() error, err error,
|
||||
) {
|
||||
tun, err := amneziatun.CreateTUN(interfaceName, int(mtu))
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("creating TUN device: %w", err)
|
||||
}
|
||||
|
||||
cleanups.Add("closing TUN device", 7, tun.Close)
|
||||
|
||||
tunName, err := tun.Name()
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("getting created TUN device name: %w", err)
|
||||
} else if tunName != interfaceName {
|
||||
return 0, nil, fmt.Errorf("TUN device name is mismatching: expected %q and got %q", interfaceName, tunName)
|
||||
}
|
||||
|
||||
link, err := netLinker.LinkByName(interfaceName)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("finding link %s: %w", interfaceName, err)
|
||||
}
|
||||
cleanups.Add("deleting link", 5, func() error {
|
||||
return netLinker.LinkDel(link.Index)
|
||||
})
|
||||
|
||||
bind := amneziaconn.NewDefaultBind()
|
||||
cleanups.Add("closing bind", 7, bind.Close)
|
||||
|
||||
deviceLogger := amneziadevice.Logger{
|
||||
Verbosef: logger.Debugf,
|
||||
Errorf: logger.Errorf,
|
||||
}
|
||||
device := amneziadevice.NewDevice(tun, bind, &deviceLogger)
|
||||
|
||||
cleanups.Add("closing Wireguard device", 6, func() error {
|
||||
device.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
uapiFile, err := wireguard.UAPIOpen(interfaceName)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("opening UAPI socket: %w", err)
|
||||
}
|
||||
cleanups.Add("closing UAPI file", 3, uapiFile.Close)
|
||||
|
||||
uapiListener, err := wireguard.UAPIListen(interfaceName, uapiFile)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listening on UAPI socket: %w", err)
|
||||
}
|
||||
cleanups.Add("closing UAPI listener", 2, uapiListener.Close)
|
||||
|
||||
uapiConfig := settings.uapiConfig()
|
||||
err = device.IpcSet(uapiConfig)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("setting amneziawg uapi config: %w", err)
|
||||
}
|
||||
|
||||
// acceptAndHandle exits when uapiListener is closed
|
||||
uapiAcceptErrorCh := make(chan error)
|
||||
go acceptAndHandle(uapiListener, device, uapiAcceptErrorCh)
|
||||
waitAndCleanup = func() error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case err = <-uapiAcceptErrorCh:
|
||||
close(uapiAcceptErrorCh)
|
||||
case <-device.Wait():
|
||||
err = errors.New("device waited for")
|
||||
}
|
||||
|
||||
cleanups.Cleanup(logger)
|
||||
|
||||
<-uapiAcceptErrorCh // wait for acceptAndHandle to exit
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return link.Index, waitAndCleanup, nil
|
||||
}
|
||||
|
||||
func acceptAndHandle(uapi net.Listener, device *amneziadevice.Device,
|
||||
uapiAcceptErrorCh chan<- error,
|
||||
) {
|
||||
for { // stopped by uapiFile.Close()
|
||||
conn, err := uapi.Accept()
|
||||
if err != nil {
|
||||
uapiAcceptErrorCh <- err
|
||||
return
|
||||
}
|
||||
go device.IpcHandle(conn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Wireguard wireguard.Settings
|
||||
JunkPacketCount uint16
|
||||
JunkPacketMin uint16
|
||||
JunkPacketMax uint16
|
||||
PaddingS1 uint16
|
||||
PaddingS2 uint16
|
||||
PaddingS3 uint16
|
||||
PaddingS4 uint16
|
||||
HeaderH1 string
|
||||
HeaderH2 string
|
||||
HeaderH3 string
|
||||
HeaderH4 string
|
||||
InitPacketI1 string
|
||||
InitPacketI2 string
|
||||
InitPacketI3 string
|
||||
InitPacketI4 string
|
||||
InitPacketI5 string
|
||||
}
|
||||
|
||||
func (s Settings) uapiConfig() string {
|
||||
uintFields := map[string]uint16{
|
||||
"jc": s.JunkPacketCount,
|
||||
"jmin": s.JunkPacketMin,
|
||||
"jmax": s.JunkPacketMax,
|
||||
"s1": s.PaddingS1,
|
||||
"s2": s.PaddingS2,
|
||||
"s3": s.PaddingS3,
|
||||
"s4": s.PaddingS4,
|
||||
}
|
||||
stringFields := map[string]string{
|
||||
"h1": s.HeaderH1,
|
||||
"h2": s.HeaderH2,
|
||||
"h3": s.HeaderH3,
|
||||
"h4": s.HeaderH4,
|
||||
"i1": s.InitPacketI1,
|
||||
"i2": s.InitPacketI2,
|
||||
"i3": s.InitPacketI3,
|
||||
"i4": s.InitPacketI4,
|
||||
"i5": s.InitPacketI5,
|
||||
}
|
||||
lines := make([]string, 0, len(uintFields)+len(stringFields))
|
||||
|
||||
for key, val := range uintFields {
|
||||
lines = append(lines, fmt.Sprintf("%s=%d", key, val))
|
||||
}
|
||||
|
||||
for key, val := range stringFields {
|
||||
lines = append(lines, key+"="+val)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Wireguard.SetDefaults()
|
||||
}
|
||||
|
||||
func (s *Settings) Check() error {
|
||||
return s.Wireguard.Check()
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package boringpoll
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
type BoringPoll struct {
|
||||
// Injected dependencies
|
||||
client *http.Client
|
||||
logger Logger
|
||||
|
||||
// Internal state
|
||||
urlToData map[string]*urlData
|
||||
|
||||
// Internal signals and channels
|
||||
cancel context.CancelFunc
|
||||
done *sync.WaitGroup
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type urlData struct{}
|
||||
|
||||
func New(client *http.Client, logger Logger, settings settings.BoringPoll) *BoringPoll {
|
||||
urlToData := make(map[string]*urlData)
|
||||
if *settings.GluetunCom {
|
||||
logger.Infof("gluetun.com is DOWN most likely thanks to you! so not doing anything anymore")
|
||||
}
|
||||
return &BoringPoll{
|
||||
client: client,
|
||||
logger: logger,
|
||||
urlToData: urlToData,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BoringPoll) Start() (runError <-chan error, err error) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if len(b.urlToData) == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
const minPeriod = time.Minute
|
||||
const maxPeriod = 5 * time.Minute
|
||||
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
|
||||
|
||||
var ready, done sync.WaitGroup
|
||||
b.done = &done
|
||||
ready.Add(len(b.urlToData))
|
||||
done.Add(len(b.urlToData))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
b.cancel = cancel
|
||||
for url := range b.urlToData {
|
||||
go func(url string) {
|
||||
defer done.Done()
|
||||
|
||||
b.logger.Infof("running against %s periodically between %s and %s "+
|
||||
"and will log every %s downloaded",
|
||||
url, minPeriod, maxPeriod, byteCountSI(logEveryBytes))
|
||||
totalDownloaded := uint64(0)
|
||||
lastDownloaded := uint64(0)
|
||||
consecutiveFails := 0
|
||||
const maxConsecutiveErrs = 3
|
||||
const coolDownTimeout = time.Hour
|
||||
timer := time.NewTimer(time.Hour)
|
||||
var err error
|
||||
|
||||
ready.Done()
|
||||
for {
|
||||
timeout := minPeriod + time.Duration(rand.Int63n(int64(maxPeriod-minPeriod))) //nolint:gosec
|
||||
if consecutiveFails >= maxConsecutiveErrs {
|
||||
b.logger.Debugf("pausing poll to %s for %s due to %d consecutive errors, last error: %s",
|
||||
url, coolDownTimeout, consecutiveFails, err)
|
||||
timeout = coolDownTimeout
|
||||
}
|
||||
timer.Reset(timeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
totalDownloaded += lastDownloaded
|
||||
if totalDownloaded > 0 {
|
||||
b.logger.Infof("stopping poll to %s, downloaded %s!", url, byteCountSI(totalDownloaded))
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
var n int64
|
||||
n, err = fetchURL(ctx, b.client, url)
|
||||
if err != nil {
|
||||
consecutiveFails++
|
||||
continue
|
||||
}
|
||||
consecutiveFails = 0
|
||||
totalDownloaded += uint64(n) //nolint:gosec
|
||||
lastDownloaded += uint64(n) //nolint:gosec
|
||||
if lastDownloaded >= logEveryBytes {
|
||||
b.logger.Infof("thanks for helping! You have downloaded %s from %s so far!",
|
||||
byteCountSI(totalDownloaded), url)
|
||||
lastDownloaded = 0
|
||||
}
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func fetchURL(ctx context.Context, client *http.Client, url string) (downloaded int64, err error) {
|
||||
const requestTimeout = 10 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return 0, err
|
||||
}
|
||||
request.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
request.Header.Set("Pragma", "no-cache")
|
||||
request.Header.Set("Expires", "0")
|
||||
request.Header.Set("User-Agent", getRandomUserAgent())
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
downloaded, err = io.Copy(io.Discard, response.Body)
|
||||
_ = response.Body.Close()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return downloaded, nil
|
||||
}
|
||||
|
||||
func getRandomUserAgent() string {
|
||||
//nolint:lll
|
||||
userAgents := [...]string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPad; CPU OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Android 14; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
|
||||
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
||||
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
||||
}
|
||||
return userAgents[rand.Intn(len(userAgents))] //nolint:gosec
|
||||
}
|
||||
|
||||
func (b *BoringPoll) Stop() error {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if b.cancel == nil {
|
||||
return nil
|
||||
}
|
||||
b.cancel()
|
||||
b.done.Wait()
|
||||
b.cancel = nil
|
||||
b.done = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func byteCountSI(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%dB", b)
|
||||
}
|
||||
|
||||
div, exp := uint64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package boringpoll
|
||||
|
||||
type Logger interface {
|
||||
Infof(format string, args ...any)
|
||||
Debugf(format string, args ...any)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cleanup
|
||||
|
||||
import "sort"
|
||||
|
||||
type Cleanups []cleanup
|
||||
|
||||
type cleanup struct {
|
||||
operation string
|
||||
orderIndex uint
|
||||
cleanup func() error
|
||||
done bool
|
||||
}
|
||||
|
||||
// Add adds a cleanup function to the list of cleanups, with a description of the
|
||||
// operation being cleaned up, and an order index that determines the order in which
|
||||
// the cleanup functions are run. The lower the order index, the earlier the cleanup
|
||||
// function is run.
|
||||
func (c *Cleanups) Add(operation string, orderIndex uint,
|
||||
cleanupFunc func() error,
|
||||
) {
|
||||
closer := cleanup{
|
||||
operation: operation,
|
||||
orderIndex: orderIndex,
|
||||
cleanup: cleanupFunc,
|
||||
}
|
||||
*c = append(*c, closer)
|
||||
}
|
||||
|
||||
// Cleanup runs the cleanup functions in the order of their orderIndex,
|
||||
// and logs any error that occurs during cleanup.
|
||||
// It can also be re-called in case a cleanup fails, and already cleaned up
|
||||
// functions will not be re-run.
|
||||
func (c *Cleanups) Cleanup(logger Logger) {
|
||||
closers := *c
|
||||
|
||||
sort.Slice(closers, func(i, j int) bool {
|
||||
return closers[i].orderIndex < closers[j].orderIndex
|
||||
})
|
||||
|
||||
for i, closer := range closers {
|
||||
if closer.done {
|
||||
continue
|
||||
}
|
||||
closers[i].done = true
|
||||
logger.Debug(closer.operation + "...")
|
||||
err := closer.cleanup()
|
||||
if err != nil {
|
||||
logger.Error("failed " + closer.operation + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Cleanups(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
var ACloseCalled, BCloseCalled, CCloseCalled bool
|
||||
var (
|
||||
AErr error
|
||||
BErr = errors.New("B failed")
|
||||
CErr = errors.New("C failed")
|
||||
)
|
||||
|
||||
var cleanups Cleanups
|
||||
cleanups.Add("cleaning up A", 5, func() error {
|
||||
ACloseCalled = true
|
||||
return AErr
|
||||
})
|
||||
|
||||
cleanups.Add("cleaning up B", 3, func() error {
|
||||
BCloseCalled = true
|
||||
return BErr
|
||||
})
|
||||
|
||||
cleanups.Add("cleaning up C", 2, func() error {
|
||||
CCloseCalled = true
|
||||
return CErr
|
||||
})
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
prevCall := logger.EXPECT().Debug("cleaning up C...")
|
||||
prevCall = logger.EXPECT().Error("failed cleaning up C: C failed").After(prevCall)
|
||||
prevCall = logger.EXPECT().Debug("cleaning up B...").After(prevCall)
|
||||
prevCall = logger.EXPECT().Error("failed cleaning up B: B failed").After(prevCall)
|
||||
logger.EXPECT().Debug("cleaning up A...").After(prevCall)
|
||||
|
||||
cleanups.Cleanup(logger)
|
||||
|
||||
cleanups.Cleanup(logger) // run twice should not close already closed
|
||||
|
||||
for _, cleanup := range cleanups {
|
||||
assert.True(t, cleanup.done)
|
||||
}
|
||||
|
||||
assert.True(t, ACloseCalled)
|
||||
assert.True(t, BCloseCalled)
|
||||
assert.True(t, CCloseCalled)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cleanup
|
||||
|
||||
type Logger interface {
|
||||
Debug(string)
|
||||
Error(string)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package cleanup
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
|
||||
@@ -0,0 +1,58 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/cleanup (interfaces: Logger)
|
||||
|
||||
// Package cleanup is a generated GoMock package.
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debug mocks base method.
|
||||
func (m *MockLogger) Debug(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Debug", arg0)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug.
|
||||
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
+1
-1
@@ -2,6 +2,6 @@ package cli
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *CLI) CI(context context.Context) error {
|
||||
func (c *CLI) CI(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-17
@@ -1,22 +1,7 @@
|
||||
// Package cli defines an interface CLI to run command line operations.
|
||||
package cli
|
||||
|
||||
var _ CLIer = (*CLI)(nil)
|
||||
|
||||
type CLIer interface {
|
||||
ClientKeyFormatter
|
||||
HealthChecker
|
||||
OpenvpnConfigMaker
|
||||
Updater
|
||||
ServersFormatter
|
||||
}
|
||||
|
||||
type CLI struct {
|
||||
repoServersPath string
|
||||
}
|
||||
type CLI struct{}
|
||||
|
||||
func New() *CLI {
|
||||
return &CLI{
|
||||
repoServersPath: "./internal/storage/servers.json",
|
||||
}
|
||||
return &CLI{}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,12 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||
)
|
||||
|
||||
type ClientKeyFormatter interface {
|
||||
ClientKey(args []string) error
|
||||
}
|
||||
|
||||
func (c *CLI) ClientKey(args []string) error {
|
||||
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
||||
filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file")
|
||||
const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate?
|
||||
filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file")
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,9 +27,6 @@ func (c *CLI) ClientKey(args []string) error {
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := string(data)
|
||||
s = strings.ReplaceAll(s, "\n", "")
|
||||
s = strings.ReplaceAll(s, "\r", "")
|
||||
|
||||
@@ -4,126 +4,100 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type ServersFormatter interface {
|
||||
FormatServers(args []string) error
|
||||
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")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrFormatNotRecognized = errors.New("format is not recognized")
|
||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||
)
|
||||
|
||||
func (c *CLI) FormatServers(args []string) error {
|
||||
var format, output string
|
||||
var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad,
|
||||
nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark,
|
||||
torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool
|
||||
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
||||
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")
|
||||
flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers")
|
||||
flagSet.BoolVar(&expressvpn, "expressvpn", false, "Format ExpressVPN servers")
|
||||
flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers")
|
||||
flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers")
|
||||
flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers")
|
||||
flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers")
|
||||
flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers")
|
||||
flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers")
|
||||
flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers")
|
||||
flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers")
|
||||
flagSet.BoolVar(&privado, "privado", false, "Format Privado servers")
|
||||
flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers")
|
||||
flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers")
|
||||
flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers")
|
||||
flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers")
|
||||
flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers")
|
||||
flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers")
|
||||
flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers")
|
||||
flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers")
|
||||
flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers")
|
||||
titleCaser := cases.Title(language.English)
|
||||
for _, provider := range allProviderFlags {
|
||||
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
||||
}
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if format != "markdown" {
|
||||
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
|
||||
}
|
||||
// Note the format is validated by storage.Format
|
||||
|
||||
logger := newNoopLogger()
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create servers storage: %w", err)
|
||||
// Verify only one provider is set to be formatted.
|
||||
var providers []string
|
||||
for provider, formatPtr := range providersToFormat {
|
||||
if *formatPtr {
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
currentServers := storage.GetServers()
|
||||
|
||||
var formatted string
|
||||
switch {
|
||||
case cyberghost:
|
||||
formatted = currentServers.Cyberghost.ToMarkdown()
|
||||
case expressvpn:
|
||||
formatted = currentServers.Expressvpn.ToMarkdown()
|
||||
case fastestvpn:
|
||||
formatted = currentServers.Fastestvpn.ToMarkdown()
|
||||
case hideMyAss:
|
||||
formatted = currentServers.HideMyAss.ToMarkdown()
|
||||
case ipvanish:
|
||||
formatted = currentServers.Ipvanish.ToMarkdown()
|
||||
case ivpn:
|
||||
formatted = currentServers.Ivpn.ToMarkdown()
|
||||
case mullvad:
|
||||
formatted = currentServers.Mullvad.ToMarkdown()
|
||||
case nordvpn:
|
||||
formatted = currentServers.Nordvpn.ToMarkdown()
|
||||
case perfectPrivacy:
|
||||
formatted = currentServers.Perfectprivacy.ToMarkdown()
|
||||
case pia:
|
||||
formatted = currentServers.Pia.ToMarkdown()
|
||||
case privado:
|
||||
formatted = currentServers.Privado.ToMarkdown()
|
||||
case privatevpn:
|
||||
formatted = currentServers.Privatevpn.ToMarkdown()
|
||||
case protonvpn:
|
||||
formatted = currentServers.Protonvpn.ToMarkdown()
|
||||
case purevpn:
|
||||
formatted = currentServers.Purevpn.ToMarkdown()
|
||||
case surfshark:
|
||||
formatted = currentServers.Surfshark.ToMarkdown()
|
||||
case torguard:
|
||||
formatted = currentServers.Torguard.ToMarkdown()
|
||||
case vpnUnlimited:
|
||||
formatted = currentServers.VPNUnlimited.ToMarkdown()
|
||||
case vyprvpn:
|
||||
formatted = currentServers.Vyprvpn.ToMarkdown()
|
||||
case wevpn:
|
||||
formatted = currentServers.Wevpn.ToMarkdown()
|
||||
case windscribe:
|
||||
formatted = currentServers.Windscribe.ToMarkdown()
|
||||
switch len(providers) {
|
||||
case 0:
|
||||
return errors.New("VPN provider to format was not specified")
|
||||
case 1:
|
||||
default:
|
||||
return ErrProviderUnspecified
|
||||
return fmt.Errorf("more than one VPN provider to format were specified: %d specified: %s", len(providers),
|
||||
strings.Join(providers, ", "))
|
||||
}
|
||||
|
||||
var providerToFormat string
|
||||
for _, providerToFormat = range allProviders {
|
||||
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
storage, err := setupStorage(newNoopLogger())
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up storage: %w", err)
|
||||
}
|
||||
|
||||
formatted, err := storage.Format(providerToFormat, format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting servers: %w", err)
|
||||
}
|
||||
|
||||
output = filepath.Clean(output)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open output file: %w", err)
|
||||
return fmt.Errorf("opening output file: %w", err)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprint(file, formatted)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("cannot write to output file: %w", err)
|
||||
return fmt.Errorf("writing to output file: %w", err)
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot close output file: %w", err)
|
||||
return fmt.Errorf("closing output file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *CLI) GenKey(args []string) (err error) {
|
||||
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
|
||||
err = flagSet.Parse(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
const keyLength = 128 / 8
|
||||
keyBytes := make([]byte, keyLength)
|
||||
|
||||
_, _ = rand.Read(keyBytes)
|
||||
|
||||
key := base58Encode(keyBytes)
|
||||
fmt.Println(key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func base58Encode(data []byte) string {
|
||||
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
const radix = 58
|
||||
|
||||
zcount := 0
|
||||
for zcount < len(data) && data[zcount] == 0 {
|
||||
zcount++
|
||||
}
|
||||
|
||||
// integer simplification of ceil(log(256)/log(58))
|
||||
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
|
||||
size := zcount + ceilLog256Div58
|
||||
|
||||
output := make([]byte, size)
|
||||
|
||||
high := size - 1
|
||||
for _, b := range data {
|
||||
i := size - 1
|
||||
for carry := uint32(b); i > high || carry != 0; i-- {
|
||||
carry += 256 * uint32(output[i]) //nolint:mnd
|
||||
output[i] = byte(carry % radix)
|
||||
carry /= radix
|
||||
}
|
||||
high = i
|
||||
}
|
||||
|
||||
// Determine the additional "zero-gap" in the output buffer
|
||||
additionalZeroGapEnd := zcount
|
||||
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
|
||||
additionalZeroGapEnd++
|
||||
}
|
||||
|
||||
val := output[additionalZeroGapEnd-zcount:]
|
||||
size = len(val)
|
||||
for i := range val {
|
||||
output[i] = alphabet[val[i]]
|
||||
}
|
||||
|
||||
return string(output[:size])
|
||||
}
|
||||
@@ -6,21 +6,21 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
)
|
||||
|
||||
type HealthChecker interface {
|
||||
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
|
||||
}
|
||||
|
||||
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
|
||||
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
|
||||
// Extract the health server port from the configuration.
|
||||
config, err := source.ReadHealth()
|
||||
var config settings.Health
|
||||
err = config.Read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
err = config.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/storage"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gosettings/reader/sources/env"
|
||||
)
|
||||
|
||||
type storageSetupLogger interface {
|
||||
storage.Logger
|
||||
files.Warner
|
||||
}
|
||||
|
||||
func setupStorage(logger storageSetupLogger) (s *storage.Storage, err error) {
|
||||
settingsReader := reader.New(reader.Settings{
|
||||
Sources: []reader.Source{
|
||||
secrets.New(logger),
|
||||
files.New(logger),
|
||||
env.New(env.Settings{}),
|
||||
},
|
||||
})
|
||||
var settings settings.Storage
|
||||
err = settings.Read(settingsReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading storage settings: %w", err)
|
||||
}
|
||||
settings.SetDefaults()
|
||||
storage, err := storage.New(logger, *settings.ServersEnabled, settings.ServersPath,
|
||||
settings.LegacyServersFilepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating storage: %w", err)
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package sources
|
||||
package cli
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
|
||||
type Source interface {
|
||||
Read() (settings settings.Settings, err error)
|
||||
ReadHealth() (settings settings.Health, err error)
|
||||
ReadHealth() (health settings.Health, err error)
|
||||
String() string
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type noopFirewall struct{}
|
||||
|
||||
func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr,
|
||||
_ uint16, _ bool,
|
||||
) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package cli
|
||||
|
||||
import "github.com/qdm12/golibs/logging"
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func newNoopLogger() *noopLogger {
|
||||
return new(noopLogger)
|
||||
}
|
||||
|
||||
func (l *noopLogger) Debug(s string) {}
|
||||
func (l *noopLogger) Info(s string) {}
|
||||
func (l *noopLogger) Warn(s string) {}
|
||||
func (l *noopLogger) Error(s string) {}
|
||||
func (l *noopLogger) PatchLevel(level logging.Level) {}
|
||||
func (l *noopLogger) PatchPrefix(prefix string) {}
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Infof(string, ...any) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
func (l *noopLogger) Warnf(string, ...any) {}
|
||||
|
||||
@@ -1,50 +1,94 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
"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 OpenvpnConfigMaker interface {
|
||||
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
|
||||
}
|
||||
|
||||
type OpenvpnConfigLogger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
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 {
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
ipv6Checker IPv6Checker,
|
||||
) error {
|
||||
storage, err := setupStorage(newNoopLogger())
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up storage: %w", err)
|
||||
}
|
||||
|
||||
var allSettings settings.Settings
|
||||
err = allSettings.Read(reader, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allServers := storage.GetServers()
|
||||
allSettings.SetDefaults()
|
||||
|
||||
allSettings, err := source.Read()
|
||||
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(),
|
||||
allSettings.IPv6.CheckAddresses, &noopFirewall{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger)
|
||||
if 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, ipv6SupportLevel == netlink.IPv6Internet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = allSettings.Validate(allServers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
|
||||
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := providerConf.OpenVPNConfig(connection,
|
||||
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
|
||||
|
||||
fmt.Println(strings.Join(lines, "\n"))
|
||||
return nil
|
||||
|
||||
+84
-73
@@ -2,79 +2,87 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
"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/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")
|
||||
ErrDNSAddress = errors.New("DNS address is not valid")
|
||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||
)
|
||||
|
||||
type Updater interface {
|
||||
Update(ctx context.Context, args []string, logger UpdaterLogger) error
|
||||
}
|
||||
|
||||
type UpdaterLogger interface {
|
||||
Info(s string)
|
||||
Infof(format string, args ...any)
|
||||
Warn(s string)
|
||||
Warnf(format string, args ...any)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{CLI: boolPtr(true)}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var dnsAddress, csvProviders string
|
||||
options := settings.Updater{}
|
||||
// TODO v4: remove flags below already present in standard settings
|
||||
var endUserMode, maintainerMode bool
|
||||
var updateAll bool
|
||||
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, 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(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
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", "",
|
||||
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "deprecated")
|
||||
flagSet.BoolVar(&maintainerMode, "maintainer", false, "deprecated")
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !endUserMode && !maintainerMode {
|
||||
return ErrModeUnspecified
|
||||
}
|
||||
|
||||
options.DNSAddress = net.ParseIP(dnsAddress)
|
||||
if options.DNSAddress == nil {
|
||||
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
|
||||
switch {
|
||||
case dnsServer != "":
|
||||
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
case endUserMode:
|
||||
logger.Warn("The -enduser flag is now unused")
|
||||
case maintainerMode:
|
||||
logger.Warn("The -maintainer flag is now unused")
|
||||
}
|
||||
|
||||
if updateAll {
|
||||
for _, provider := range constants.AllProviders() {
|
||||
if provider == constants.Custom {
|
||||
continue
|
||||
}
|
||||
options.Providers = append(options.Providers, provider)
|
||||
}
|
||||
options.Providers = providers.All()
|
||||
} else {
|
||||
if csvProviders == "" {
|
||||
return ErrNoProviderSpecified
|
||||
return errors.New("no provider was specified")
|
||||
}
|
||||
options.Providers = strings.Split(csvProviders, ",")
|
||||
}
|
||||
|
||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||
if protonEmail == "" && protonUsername != "" {
|
||||
protonEmail = protonUsername + "@protonmail.com"
|
||||
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
||||
"This assumes the email is " + protonEmail + " and may not work.")
|
||||
}
|
||||
options.ProtonEmail = &protonEmail
|
||||
options.ProtonPassword = &protonPassword
|
||||
}
|
||||
|
||||
options.SetDefaults(options.Providers[0])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -82,48 +90,51 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return fmt.Errorf("options validation failed: %w", err)
|
||||
}
|
||||
|
||||
storage, err := setupStorage(logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
|
||||
dohSettings := doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{
|
||||
dnsprovider.Cloudflare(),
|
||||
dnsprovider.Google(),
|
||||
},
|
||||
}
|
||||
dnsDialer, err := doh.New(dohSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
||||
}
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: dnsDialer.Dial,
|
||||
}
|
||||
|
||||
const clientTimeout = 10 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(dnsDialer)
|
||||
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("cannot create servers storage: %w", err)
|
||||
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||
}
|
||||
currentServers := storage.GetServers()
|
||||
ipFetcher := api.NewResilient(fetchers, logger)
|
||||
|
||||
updater := updater.New(options, httpClient, currentServers, logger)
|
||||
allServers, err := updater.UpdateServers(ctx)
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger, *options.PreferDirectDownload)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update server information: %w", err)
|
||||
}
|
||||
|
||||
if endUserMode {
|
||||
if err := storage.FlushToFile(allServers); err != nil {
|
||||
return fmt.Errorf("cannot write updated information to file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if maintainerMode {
|
||||
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
|
||||
return fmt.Errorf("cannot write updated information to file: %w", err)
|
||||
}
|
||||
return fmt.Errorf("updating server information: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeToEmbeddedJSON(repoServersPath string,
|
||||
allServers models.AllServers) error {
|
||||
const perms = 0600
|
||||
f, err := os.OpenFile(repoServersPath,
|
||||
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(allServers)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package command
|
||||
|
||||
// Cmder handles running subprograms synchronously and asynchronously.
|
||||
type Cmder struct{}
|
||||
|
||||
func New() *Cmder {
|
||||
return &Cmder{}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package healthcheck
|
||||
package command
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package command
|
||||
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 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, errors.New("command is empty")
|
||||
}
|
||||
|
||||
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("unterminated backslash-escape: %q", 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, errors.New("unterminated double-quoted string")
|
||||
}
|
||||
|
||||
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, errors.New("unterminated single-quoted string")
|
||||
}
|
||||
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, errors.New("unterminated backslash-escape")
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
command: "",
|
||||
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",
|
||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||
},
|
||||
"unterminated_double_quote": {
|
||||
command: "\"abc'def",
|
||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||
},
|
||||
"unterminated_escape": {
|
||||
command: "abc\\",
|
||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||
},
|
||||
"unterminated_escape_only": {
|
||||
command: " \\",
|
||||
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)
|
||||
if testCase.errMessage != "" {
|
||||
assert.ErrorContains(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Start launches a command and streams stdout and stderr to channels.
|
||||
// stdoutLines and stderrLines channels will be closed when there is no more
|
||||
// output to read, in order for the caller to catch all lines even after the
|
||||
// command has finished. The waitError channel returned will never be closed.
|
||||
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,
|
||||
) {
|
||||
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, stdoutDone, stdout, stdoutLinesCh)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stderr.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
waitErrorCh := make(chan error)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stdout.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
_ = stderr.Close()
|
||||
waitErrorCh <- err
|
||||
}()
|
||||
|
||||
<-stdoutReady
|
||||
<-stderrReady
|
||||
|
||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||
}
|
||||
|
||||
func streamToChannel(ready 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
|
||||
}
|
||||
lines <- "stream error: " + err.Error()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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)
|
||||
|
||||
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
|
||||
testCase.stdout, testCase.stderr, testCase.waitErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
stdoutIndex := 0
|
||||
stderrIndex := 0
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case line, ok := <-stdoutLines:
|
||||
if !ok {
|
||||
stdoutLines = nil
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expectedStdout[stdoutIndex], line)
|
||||
stdoutIndex++
|
||||
case line, ok := <-stderrLines:
|
||||
if !ok {
|
||||
stderrLines = nil
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expectedStderr[stderrIndex], line)
|
||||
stderrIndex++
|
||||
case err := <-waitError:
|
||||
if expectedWaitErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, expectedWaitErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expectedStdout), stdoutIndex)
|
||||
assert.Equal(t, len(expectedStderr), stderrIndex)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (c *Cmder) RunAndLog(ctx context.Context, command string, logger Logger) (err error) {
|
||||
args, err := split(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing command: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
||||
stdout, stderr, waitError, err := c.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(done chan<- struct{}, logger Logger,
|
||||
stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
for {
|
||||
select {
|
||||
case line, ok := <-stdout:
|
||||
if ok {
|
||||
logger.Info(line)
|
||||
break
|
||||
}
|
||||
if stderr == nil {
|
||||
return
|
||||
}
|
||||
stdout = nil
|
||||
case line, ok := <-stderr:
|
||||
if ok {
|
||||
logger.Error(line)
|
||||
break
|
||||
}
|
||||
if stdout == nil {
|
||||
return
|
||||
}
|
||||
stderr = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type AmneziaWg struct {
|
||||
// Wireguard contains the configuration for Wireguard, given
|
||||
// AmneziaWg is based on Wireguard
|
||||
Wireguard Wireguard `json:"wireguard"`
|
||||
JunkPacketCount *uint16 `json:"junk_packet_count"`
|
||||
JunkPacketMin *uint16 `json:"junk_packet_min"`
|
||||
JunkPacketMax *uint16 `json:"junk_packet_max"`
|
||||
PaddingS1 *uint16 `json:"padding_s1"`
|
||||
PaddingS2 *uint16 `json:"padding_s2"`
|
||||
PaddingS3 *uint16 `json:"padding_s3"`
|
||||
PaddingS4 *uint16 `json:"padding_s4"`
|
||||
HeaderH1 *string `json:"header_h1"`
|
||||
HeaderH2 *string `json:"header_h2"`
|
||||
HeaderH3 *string `json:"header_h3"`
|
||||
HeaderH4 *string `json:"header_h4"`
|
||||
InitPacketI1 *string `json:"init_packet_i1"`
|
||||
InitPacketI2 *string `json:"init_packet_i2"`
|
||||
InitPacketI3 *string `json:"init_packet_i3"`
|
||||
InitPacketI4 *string `json:"init_packet_i4"`
|
||||
InitPacketI5 *string `json:"init_packet_i5"`
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) read(r *reader.Reader) (err error) {
|
||||
const amneziawg = true
|
||||
err = a.Wireguard.read(r, amneziawg)
|
||||
if err != nil {
|
||||
return err // do not wrap this error
|
||||
}
|
||||
|
||||
uint16Fields := map[string]**uint16{
|
||||
"AMNEZIAWG_JC": &a.JunkPacketCount,
|
||||
"AMNEZIAWG_JMIN": &a.JunkPacketMin,
|
||||
"AMNEZIAWG_JMAX": &a.JunkPacketMax,
|
||||
"AMNEZIAWG_S1": &a.PaddingS1,
|
||||
"AMNEZIAWG_S2": &a.PaddingS2,
|
||||
"AMNEZIAWG_S3": &a.PaddingS3,
|
||||
"AMNEZIAWG_S4": &a.PaddingS4,
|
||||
}
|
||||
for key, dst := range uint16Fields {
|
||||
*dst, err = r.Uint16Ptr(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stringFields := map[string]**string{
|
||||
"AMNEZIAWG_H1": &a.HeaderH1,
|
||||
"AMNEZIAWG_H2": &a.HeaderH2,
|
||||
"AMNEZIAWG_H3": &a.HeaderH3,
|
||||
"AMNEZIAWG_H4": &a.HeaderH4,
|
||||
"AMNEZIAWG_I1": &a.InitPacketI1,
|
||||
"AMNEZIAWG_I2": &a.InitPacketI2,
|
||||
"AMNEZIAWG_I3": &a.InitPacketI3,
|
||||
"AMNEZIAWG_I4": &a.InitPacketI4,
|
||||
"AMNEZIAWG_I5": &a.InitPacketI5,
|
||||
}
|
||||
opt := reader.ForceLowercase(false)
|
||||
for key, dst := range stringFields {
|
||||
*dst = r.Get(key, opt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AmneziaWg) copy() (copied AmneziaWg) {
|
||||
return AmneziaWg{
|
||||
Wireguard: a.Wireguard.copy(),
|
||||
JunkPacketCount: gosettings.CopyPointer(a.JunkPacketCount),
|
||||
JunkPacketMin: gosettings.CopyPointer(a.JunkPacketMin),
|
||||
JunkPacketMax: gosettings.CopyPointer(a.JunkPacketMax),
|
||||
PaddingS1: gosettings.CopyPointer(a.PaddingS1),
|
||||
PaddingS2: gosettings.CopyPointer(a.PaddingS2),
|
||||
PaddingS3: gosettings.CopyPointer(a.PaddingS3),
|
||||
PaddingS4: gosettings.CopyPointer(a.PaddingS4),
|
||||
HeaderH1: gosettings.CopyPointer(a.HeaderH1),
|
||||
HeaderH2: gosettings.CopyPointer(a.HeaderH2),
|
||||
HeaderH3: gosettings.CopyPointer(a.HeaderH3),
|
||||
HeaderH4: gosettings.CopyPointer(a.HeaderH4),
|
||||
InitPacketI1: gosettings.CopyPointer(a.InitPacketI1),
|
||||
InitPacketI2: gosettings.CopyPointer(a.InitPacketI2),
|
||||
InitPacketI3: gosettings.CopyPointer(a.InitPacketI3),
|
||||
InitPacketI4: gosettings.CopyPointer(a.InitPacketI4),
|
||||
InitPacketI5: gosettings.CopyPointer(a.InitPacketI5),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) overrideWith(other AmneziaWg) {
|
||||
a.Wireguard.overrideWith(other.Wireguard)
|
||||
a.JunkPacketCount = gosettings.OverrideWithPointer(a.JunkPacketCount, other.JunkPacketCount)
|
||||
a.JunkPacketMin = gosettings.OverrideWithPointer(a.JunkPacketMin, other.JunkPacketMin)
|
||||
a.JunkPacketMax = gosettings.OverrideWithPointer(a.JunkPacketMax, other.JunkPacketMax)
|
||||
a.PaddingS1 = gosettings.OverrideWithPointer(a.PaddingS1, other.PaddingS1)
|
||||
a.PaddingS2 = gosettings.OverrideWithPointer(a.PaddingS2, other.PaddingS2)
|
||||
a.PaddingS3 = gosettings.OverrideWithPointer(a.PaddingS3, other.PaddingS3)
|
||||
a.PaddingS4 = gosettings.OverrideWithPointer(a.PaddingS4, other.PaddingS4)
|
||||
a.HeaderH1 = gosettings.OverrideWithPointer(a.HeaderH1, other.HeaderH1)
|
||||
a.HeaderH2 = gosettings.OverrideWithPointer(a.HeaderH2, other.HeaderH2)
|
||||
a.HeaderH3 = gosettings.OverrideWithPointer(a.HeaderH3, other.HeaderH3)
|
||||
a.HeaderH4 = gosettings.OverrideWithPointer(a.HeaderH4, other.HeaderH4)
|
||||
a.InitPacketI1 = gosettings.OverrideWithPointer(a.InitPacketI1, other.InitPacketI1)
|
||||
a.InitPacketI2 = gosettings.OverrideWithPointer(a.InitPacketI2, other.InitPacketI2)
|
||||
a.InitPacketI3 = gosettings.OverrideWithPointer(a.InitPacketI3, other.InitPacketI3)
|
||||
a.InitPacketI4 = gosettings.OverrideWithPointer(a.InitPacketI4, other.InitPacketI4)
|
||||
a.InitPacketI5 = gosettings.OverrideWithPointer(a.InitPacketI5, other.InitPacketI5)
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) setDefaults(vpnProvider string) {
|
||||
a.Wireguard.setDefaults(vpnProvider)
|
||||
a.Wireguard.Implementation = "userspace" // unused except in logs
|
||||
a.JunkPacketCount = gosettings.DefaultPointer(a.JunkPacketCount, 0)
|
||||
a.JunkPacketMin = gosettings.DefaultPointer(a.JunkPacketMin, 0)
|
||||
a.JunkPacketMax = gosettings.DefaultPointer(a.JunkPacketMax, 0)
|
||||
a.PaddingS1 = gosettings.DefaultPointer(a.PaddingS1, 0)
|
||||
a.PaddingS2 = gosettings.DefaultPointer(a.PaddingS2, 0)
|
||||
a.PaddingS3 = gosettings.DefaultPointer(a.PaddingS3, 0)
|
||||
a.PaddingS4 = gosettings.DefaultPointer(a.PaddingS4, 0)
|
||||
a.HeaderH1 = gosettings.DefaultPointer(a.HeaderH1, "")
|
||||
a.HeaderH2 = gosettings.DefaultPointer(a.HeaderH2, "")
|
||||
a.HeaderH3 = gosettings.DefaultPointer(a.HeaderH3, "")
|
||||
a.HeaderH4 = gosettings.DefaultPointer(a.HeaderH4, "")
|
||||
a.InitPacketI1 = gosettings.DefaultPointer(a.InitPacketI1, "")
|
||||
a.InitPacketI2 = gosettings.DefaultPointer(a.InitPacketI2, "")
|
||||
a.InitPacketI3 = gosettings.DefaultPointer(a.InitPacketI3, "")
|
||||
a.InitPacketI4 = gosettings.DefaultPointer(a.InitPacketI4, "")
|
||||
a.InitPacketI5 = gosettings.DefaultPointer(a.InitPacketI5, "")
|
||||
}
|
||||
|
||||
func (a AmneziaWg) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("AmneziaWG settings:")
|
||||
node.AppendNode(a.Wireguard.toLinesNode())
|
||||
|
||||
uintFields := []struct {
|
||||
key string
|
||||
val *uint16
|
||||
}{
|
||||
{"JC", a.JunkPacketCount},
|
||||
{"JMIN", a.JunkPacketMin},
|
||||
{"JMAX", a.JunkPacketMax},
|
||||
{"S1", a.PaddingS1},
|
||||
{"S2", a.PaddingS2},
|
||||
{"S3", a.PaddingS3},
|
||||
{"S4", a.PaddingS4},
|
||||
}
|
||||
for _, f := range uintFields {
|
||||
node.Appendf("%s: %d", f.key, *f.val)
|
||||
}
|
||||
|
||||
stringFields := []struct {
|
||||
key string
|
||||
val *string
|
||||
}{
|
||||
{"H1", a.HeaderH1},
|
||||
{"H2", a.HeaderH2},
|
||||
{"H3", a.HeaderH3},
|
||||
{"H4", a.HeaderH4},
|
||||
{"I1", a.InitPacketI1},
|
||||
{"I2", a.InitPacketI2},
|
||||
{"I3", a.InitPacketI3},
|
||||
{"I4", a.InitPacketI4},
|
||||
{"I5", a.InitPacketI5},
|
||||
}
|
||||
for _, f := range stringFields {
|
||||
node.Appendf("%s: %s", f.key, *f.val)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (a AmneziaWg) validate(vpnProvider string, ipv6Supported bool) error {
|
||||
const amneziaWG = true
|
||||
err := a.Wireguard.validate(vpnProvider, ipv6Supported, amneziaWG)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard settings: %w", err)
|
||||
}
|
||||
|
||||
if *a.JunkPacketCount == 0 {
|
||||
if *a.JunkPacketMin != 0 || *a.JunkPacketMax != 0 {
|
||||
return fmt.Errorf("junk packet count must be set when junk packet min or max is set: "+
|
||||
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
}
|
||||
} else {
|
||||
if *a.JunkPacketMin == 0 || *a.JunkPacketMax == 0 {
|
||||
return fmt.Errorf("junk packet min and max must be set when junk packet count is set: "+
|
||||
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
} else if *a.JunkPacketMin > *a.JunkPacketMax {
|
||||
return fmt.Errorf("junk packet minimum must be lower than or equal to maximum: "+
|
||||
"jmin=%d and jmax=%d", *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
}
|
||||
}
|
||||
|
||||
nameToHeaderRange := map[string]string{
|
||||
"h1": *a.HeaderH1,
|
||||
"h2": *a.HeaderH2,
|
||||
"h3": *a.HeaderH3,
|
||||
"h4": *a.HeaderH4,
|
||||
}
|
||||
for name, headerRange := range nameToHeaderRange {
|
||||
if headerRange == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(headerRange, "-")
|
||||
switch len(fields) {
|
||||
case 1:
|
||||
_, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("header range is malformed: "+
|
||||
"%s value %s is not a number", name, headerRange)
|
||||
}
|
||||
case 2: //nolint:mnd
|
||||
for _, field := range fields {
|
||||
_, err := strconv.Atoi(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("header range is malformed: "+
|
||||
"%s value %s is not a valid range", name, headerRange)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("header range is malformed: "+
|
||||
"%s value %s must be in the form n or n-m", name, headerRange)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type BoringPoll struct {
|
||||
GluetunCom *bool
|
||||
}
|
||||
|
||||
func (b BoringPoll) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BoringPoll) Copy() BoringPoll {
|
||||
return BoringPoll{
|
||||
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BoringPoll) overrideWith(other BoringPoll) {
|
||||
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
|
||||
}
|
||||
|
||||
func (b *BoringPoll) setDefaults() {
|
||||
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
|
||||
}
|
||||
|
||||
func (b BoringPoll) String() string {
|
||||
return b.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (b BoringPoll) toLinesNode() *gotree.Node {
|
||||
if !*b.GluetunCom {
|
||||
return nil
|
||||
}
|
||||
|
||||
node := gotree.New("Boring-poll settings:")
|
||||
node.Append("gluetun.com: on")
|
||||
return node
|
||||
}
|
||||
|
||||
func (b *BoringPoll) read(r *reader.Reader) (err error) {
|
||||
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
|
||||
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