mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 20:34:02 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98aa31b5f5
|
||
|
|
f32ca9f568
|
||
|
|
c80d3452f0
|
||
|
|
9bf7222208 | ||
|
|
5953f9320c | ||
|
|
170346bb74 | ||
|
|
68a4685ec6 | ||
|
|
602f85d24d | ||
|
|
d0412b21f6 | ||
|
|
0c1d001949 | ||
|
|
d7249756e2 | ||
|
|
2fa0e5ed94 | ||
|
|
fb94d4a78b | ||
|
|
b5799500e4 | ||
|
|
437dacfaab | ||
|
|
0de8b28de8 | ||
|
|
2f62e8055d | ||
|
|
b58ac669d5 | ||
|
|
60ab083def | ||
|
|
88f33debab | ||
|
|
ac40c94820 | ||
|
|
102f8a6cce | ||
|
|
83b43aecc1 | ||
|
|
5bf218f9ab | ||
|
|
efc65f317e | ||
|
|
39ab5570a8 | ||
|
|
eb564936c7 | ||
|
|
74a81a986a | ||
|
|
bec321d190 | ||
|
|
45f958e527 | ||
|
|
5f81ae3743 | ||
|
|
3a68ea5f2d | ||
|
|
dbced77566 | ||
|
|
f4f969e702 | ||
|
|
e8368f7645 | ||
|
|
38abee7d7f | ||
|
|
a3663fe8b5 | ||
|
|
2aa3321bd4 | ||
|
|
3793558c4c | ||
|
|
b6427ee321 | ||
|
|
0c9fa5e710 | ||
|
|
1fcec38aea | ||
|
|
89930631cf | ||
|
|
eedee63143 | ||
|
|
73c6a3aa37 | ||
|
|
e54d9d60d3 | ||
|
|
a2de52f071 | ||
|
|
b926a0590c | ||
|
|
58e8c8f982 | ||
|
|
4642546b35 | ||
|
|
4627910238 | ||
|
|
0840c7e3e5 | ||
|
|
de48e177b1 | ||
|
|
3ed09146b9 | ||
|
|
018bd2fdc1 | ||
|
|
0ad3a06863 | ||
|
|
d3a090d6b4 | ||
|
|
1725a0d721 | ||
|
|
87988326ab | ||
|
|
ec271baab0 | ||
|
|
139db15e83 | ||
|
|
0c030646f9 | ||
|
|
822560bede | ||
|
|
9917f61bc8 | ||
|
|
735466b90d | ||
|
|
6b51de8305 | ||
|
|
7b62e06e36 | ||
|
|
2b07c0037e | ||
|
|
450381ee16 | ||
|
|
46c33f1532 | ||
|
|
f355512aa6 | ||
|
|
289bb283b1 | ||
|
|
836090ebdf | ||
|
|
01402bdba2 | ||
|
|
026ec74dfd | ||
|
|
cc4b6ce2f4 | ||
|
|
9dfd992c1d | ||
|
|
80213ad35d | ||
|
|
d32e8e8b97 | ||
|
|
60c57c2306 | ||
|
|
0edd5e6f92 | ||
|
|
a8e4acb6f8 | ||
|
|
27d10e6820 | ||
|
|
b47e13556e | ||
|
|
de81ed565d | ||
|
|
5adfee5dd4 | ||
|
|
b0d37de0ec | ||
|
|
0cb25ba7ff | ||
|
|
614acd7303 | ||
|
|
4f5368aa2a | ||
|
|
cfb5fe66be | ||
|
|
fb390d3417 | ||
|
|
f0ae4ce290 | ||
|
|
b6b900e430 | ||
|
|
8154f65e0e | ||
|
|
a60523fed0 | ||
|
|
63b147c287 | ||
|
|
21c0d18c7c | ||
|
|
8f0bf47d56 | ||
|
|
d01e089f54 | ||
|
|
c736881792 | ||
|
|
d5a118f125 | ||
|
|
d79a8f8406 | ||
|
|
97932758d1 | ||
|
|
1f7d1c0eea | ||
|
|
8c73dde928 | ||
|
|
ded3fe26b9 | ||
|
|
2f00adfe91 | ||
|
|
049bee3d84 | ||
|
|
4fbabfda2a | ||
|
|
9ba6df0d1c | ||
|
|
4a8d099aca | ||
|
|
feb57004e1 | ||
|
|
cb436efd87 | ||
|
|
24148ea95c | ||
|
|
724904f50d | ||
|
|
a23ae05f3b | ||
|
|
b153a55149 | ||
|
|
913a38d13a | ||
|
|
81703233b0 | ||
|
|
eb7720b11e | ||
|
|
df7ddc3d6a | ||
|
|
3bc1e415f9 | ||
|
|
306fa19ad6 | ||
|
|
079252d810 | ||
|
|
d98d5be3a7 | ||
|
|
94ac9d6ffa | ||
|
|
c036558df4 | ||
|
|
4167dec5e1 | ||
|
|
57529904e3 | ||
|
|
ed4e511560 | ||
|
|
bb90bcc127 | ||
|
|
879ebd132d | ||
|
|
58e6dfd603 | ||
|
|
e675baa860 | ||
|
|
4a17849763 | ||
|
|
7adfc0352e | ||
|
|
a5d602b538 | ||
|
|
b35c212d65 | ||
|
|
45b0964afd | ||
|
|
6d8d2961e8 | ||
|
|
4138cc6494 | ||
|
|
33c0fa9bf7 | ||
|
|
21d7522356 | ||
|
|
ea71fe81b2 | ||
|
|
0871c2d790 | ||
|
|
8df1629882 | ||
|
|
ad945c77a9 | ||
|
|
a051518def | ||
|
|
4dca1d2b07 | ||
|
|
287a794772 | ||
|
|
d4822989ec | ||
|
|
d6a17c502f | ||
|
|
3db1be0687 | ||
|
|
991346621c | ||
|
|
cc63e762e3 | ||
|
|
a85348d6be | ||
|
|
7762cf3a55 | ||
|
|
62cba24071 | ||
|
|
94e4179fb7 | ||
|
|
6493688282 | ||
|
|
ffd3ab03cc | ||
|
|
00403e3a94 | ||
|
|
0bfc1ef2d4 | ||
|
|
7aa01dcebe | ||
|
|
1c0e847247 | ||
|
|
83ff4ee266 | ||
|
|
7a58c74cfe | ||
|
|
d43d6692d7 | ||
|
|
75392941da | ||
|
|
25ad776b6f | ||
|
|
5557393b38 | ||
|
|
bb49c6a55d | ||
|
|
37f8d18be5 | ||
|
|
59557059df | ||
|
|
1182b9ef6f | ||
|
|
c886ffdd81 | ||
|
|
e9b38a5b74 | ||
|
|
036b10be67 | ||
|
|
23aa2eefad | ||
|
|
ddc34bf918 | ||
|
|
ffe052617b | ||
|
|
700417b1d0 | ||
|
|
897e6bf505 | ||
|
|
317d7380cb | ||
|
|
1151291535 | ||
|
|
42927c8bdc | ||
|
|
282896be09 | ||
|
|
3809f034ad | ||
|
|
6feef02d08 | ||
|
|
5549a82d19 | ||
|
|
189dce07c6 | ||
|
|
449f84133a | ||
|
|
9c122f942f | ||
|
|
42f612f49e | ||
|
|
d7db8ca98b | ||
|
|
1cb225f52c | ||
|
|
af72b2a574 | ||
|
|
2cbee5d453 | ||
|
|
cde313b359 | ||
|
|
58cb0b2caf | ||
|
|
bb320e9d89 | ||
|
|
f6d2f2ffd8 | ||
|
|
5fe3fdd73c | ||
|
|
5b91edf5c4 | ||
|
|
8b34c1b104 | ||
|
|
36c766b331 | ||
|
|
e4a9a96309 | ||
|
|
94d46d2c65 | ||
|
|
908842063a | ||
|
|
e50cee5748 | ||
|
|
ee524abdb5 | ||
|
|
3e75e4fa63 | ||
|
|
140e9dfc2e | ||
|
|
d0065d35c2 | ||
|
|
45ce5c2f61 | ||
|
|
21129b6e00 | ||
|
|
bf38f9f8af | ||
|
|
432e530f68 | ||
|
|
a2bf9a269a | ||
|
|
cf3437bb63 | ||
|
|
d0e99dda2b | ||
|
|
ac3bd16e83 | ||
|
|
222cef8c41 | ||
|
|
ed5da2864a | ||
|
|
08393e426a | ||
|
|
0440ccf4ff | ||
|
|
a0aabf2391 | ||
|
|
3b03c4a90a | ||
|
|
80b9159ce9 | ||
|
|
bdabb0e59a | ||
|
|
543f5cde9c | ||
|
|
1a247d18b1 | ||
|
|
5a63c7d5b4 | ||
|
|
be398f9c79 | ||
|
|
4c029fc683 | ||
|
|
3066672353 | ||
|
|
ae88c0cab0 | ||
|
|
816b7f72b6 | ||
|
|
1f7584c108 | ||
|
|
2fb025bbfd | ||
|
|
c7ab53134a | ||
|
|
f83ee17361 | ||
|
|
36546cec2f | ||
|
|
074c4017f5 | ||
|
|
8e87405d3e | ||
|
|
82679ec20f | ||
|
|
308e372a5d | ||
|
|
836a481026 | ||
|
|
0a5a45b32d | ||
|
|
852ca713c8 | ||
|
|
301bde88ac | ||
|
|
6db6415798 | ||
|
|
0faf482e52 | ||
|
|
3e4faf6ba6 | ||
|
|
f7f6a7637c | ||
|
|
130b02013b | ||
|
|
1b4c777ed3 | ||
|
|
60b4b5ad8f | ||
|
|
a4930a596a | ||
|
|
fb3e921bc6 | ||
|
|
afbf4f6f74 | ||
|
|
592b7c5b87 | ||
|
|
c170887499 | ||
|
|
ad76304b43 | ||
|
|
5e38819506 | ||
|
|
f06fd5ca77 | ||
|
|
ad88afeecd | ||
|
|
c74c0c92c6 | ||
|
|
e3dc03b4db | ||
|
|
ecba88d2e3 | ||
|
|
37de052feb | ||
|
|
2041f3154b | ||
|
|
b1728c3474 | ||
|
|
b72af2b953 | ||
|
|
75c317f35e | ||
|
|
795dabb80b | ||
|
|
01b0166995 | ||
|
|
e9a147c836 | ||
|
|
de66835657 | ||
|
|
75f11965d8 | ||
|
|
e68d0c7da5 | ||
|
|
051180a170 | ||
|
|
b4e0143cb7 | ||
|
|
269852a4f1 | ||
|
|
bd8a7ed8c9 | ||
|
|
7e3e4a611d | ||
|
|
db2be4001b | ||
|
|
05c99aee4a | ||
|
|
53dde2aafa | ||
|
|
36dad5a2f6 | ||
|
|
6a19ded78e | ||
|
|
008e17cdff | ||
|
|
5ad64390d6 | ||
|
|
f9ad93dd06 | ||
|
|
77edea0e18 | ||
|
|
79f921fde9 | ||
|
|
1cdaaa16b0 | ||
|
|
5852f48ca5 | ||
|
|
0e80222990 | ||
|
|
c07e3d573d | ||
|
|
f192a718f4 | ||
|
|
0c4523d2c8 | ||
|
|
b0063a4a25 | ||
|
|
6dc494e7d0 | ||
|
|
534d5b755e | ||
|
|
aa7e488a3a | ||
|
|
eba3673e27 | ||
|
|
a27facaa16 | ||
|
|
78a9ff18a5 | ||
|
|
c0899d0743 | ||
|
|
12563219e6 | ||
|
|
59b5ff4080 | ||
|
|
d7e554540d | ||
|
|
1575b82688 | ||
|
|
5df1f594cc | ||
|
|
02ad052c49 | ||
|
|
2b8c7825ca | ||
|
|
75357b3e3c | ||
|
|
d8b14dc765 | ||
|
|
e0850869ba | ||
|
|
9375552180 | ||
|
|
cbe5b8c94e | ||
|
|
63b425f245 | ||
|
|
b53ead4372 | ||
|
|
2cdd66c722 | ||
|
|
2ae0101d18 | ||
|
|
6da9c2e58d | ||
|
|
6d89f14c9b | ||
|
|
442e2da330 | ||
|
|
a9b3560b25 | ||
|
|
880dd04728 | ||
|
|
8ea1aa3f5e | ||
|
|
4e5a83cfe9 | ||
|
|
5282ca26f3 | ||
|
|
2077db1f1e | ||
|
|
1a9bc80091 | ||
|
|
30170b9413 | ||
|
|
4687a7c899 | ||
|
|
d467fba674 | ||
|
|
6e447b1d59 | ||
|
|
b15a8ec4a5 | ||
|
|
6bef4df091 | ||
|
|
0ce0c668b9 | ||
|
|
01e201365b | ||
|
|
0b52367a82 | ||
|
|
25c842daf1 | ||
|
|
4c543aaea2 | ||
|
|
fee133a62f | ||
|
|
7d38fec74e | ||
|
|
15bb5be6c4 | ||
|
|
ad8c09a2a3 | ||
|
|
2f626e2138 | ||
|
|
93bed24a0b | ||
|
|
219235e181 | ||
|
|
79f54a4e67 | ||
|
|
4b78e83be7 | ||
|
|
ad30eca406 | ||
|
|
ecf947b86c | ||
|
|
5be581154e | ||
|
|
bc3b517e8b | ||
|
|
3f8f96b91f | ||
|
|
ef55fbba15 | ||
|
|
e7416bc04d | ||
|
|
9a6264a89f | ||
|
|
33e0509c5a | ||
|
|
ffad717829 | ||
|
|
7b1f86b75d | ||
|
|
a5e59d9ef7 | ||
|
|
16c06f247c | ||
|
|
0ddaabb136 | ||
|
|
558fec60de | ||
|
|
cc101c9a47 | ||
|
|
e6fa69d288 | ||
|
|
c14a2329c5 | ||
|
|
4c75066ef8 | ||
|
|
ce8163d1b7 | ||
|
|
ca77157fd5 | ||
|
|
66f4d967e7 | ||
|
|
d19cfb1df4 | ||
|
|
3540408adf | ||
|
|
7917434a37 | ||
|
|
853395106b | ||
|
|
d1b0d1f133 | ||
|
|
73bd7287f0 | ||
|
|
31b8ab4482 | ||
|
|
bae5407372 | ||
|
|
4814b0fcc1 | ||
|
|
3ce549bb16 | ||
|
|
5c636a68dc | ||
|
|
9f6f906786 | ||
|
|
eb32766c1f | ||
|
|
787d72cf52 | ||
|
|
ce8f1ebb6c | ||
|
|
cd29f3e20b | ||
|
|
3e105f2beb | ||
|
|
6bbdd99e7f | ||
|
|
894c68019e | ||
|
|
98f18fc22b | ||
|
|
01b739aa7c | ||
|
|
dbaa743e03 | ||
|
|
d1e5f9d145 | ||
|
|
686f177ab9 | ||
|
|
706aef8ca1 | ||
|
|
14dfb9506a | ||
|
|
f742066c54 | ||
|
|
fbe4d32590 | ||
|
|
d0f18be91d | ||
|
|
929b73e2eb | ||
|
|
6b7364238a | ||
|
|
866906e770 | ||
|
|
b3c8c4a47d | ||
|
|
fbc7499cda | ||
|
|
ca5800cf60 | ||
|
|
00bb7be900 | ||
|
|
e8c70603f7 | ||
|
|
9d72904508 | ||
|
|
456ed5b051 | ||
|
|
4b7be8c565 | ||
|
|
8758208de6 | ||
|
|
6cd91600b5 | ||
|
|
bd39fa63cb | ||
|
|
eaa757b6d0 | ||
|
|
dc99dd165e | ||
|
|
7c70fd8079 | ||
|
|
dcbbb49607 | ||
|
|
1050ca0b97 | ||
|
|
c85c88efd6 | ||
|
|
c53364d952 | ||
|
|
ed91290e47 | ||
|
|
3fd5e9eb19 | ||
|
|
87ed1d1aa7 | ||
|
|
ec4f0656fb | ||
|
|
0c5d487fae | ||
|
|
889ab6c227 | ||
|
|
e1864377c2 | ||
|
|
4c8925ab65 | ||
|
|
bf34f0c92d | ||
|
|
604b1d2597 | ||
|
|
b19f491fc8 | ||
|
|
16f9ec690b | ||
|
|
fec60083a3 | ||
|
|
0882d45d6a | ||
|
|
86c6541e0d | ||
|
|
cc1f60c4a6 | ||
|
|
700f71bb31 | ||
|
|
097b52c8ef | ||
|
|
c3ffd7a307 | ||
|
|
fe3b0f3ca5 | ||
|
|
2273a0e4af | ||
|
|
115510985a | ||
|
|
2b5b325722 | ||
|
|
c536cbecd5 | ||
|
|
5e9d0649a2 | ||
|
|
585ebfeb50 | ||
|
|
81bca75c25 | ||
|
|
4f048a3b82 | ||
|
|
43f5d2131e | ||
|
|
e2073f0585 | ||
|
|
0652f322fa | ||
|
|
d3aa0f2de5 | ||
|
|
40c6cf294a | ||
|
|
4c38ea2b11 | ||
|
|
e6d444546f | ||
|
|
611583ba88 | ||
|
|
463af648ce | ||
|
|
b748603096 | ||
|
|
54a7c6a2a5 | ||
|
|
a3bae795c4 | ||
|
|
372c703ad6 | ||
|
|
0dd890a09d | ||
|
|
0274b3436a | ||
|
|
04b88cc864 | ||
|
|
b3112c5c67 | ||
|
|
3661fe108d | ||
|
|
5d5b77d73b | ||
|
|
7bffdcdf4a | ||
|
|
ea7033d569 | ||
|
|
24fdec6694 | ||
|
|
2fe04c7c9b | ||
|
|
7b5cb8e14b | ||
|
|
8566cede74 | ||
|
|
2dc30c5837 | ||
|
|
2cdff017e2 | ||
|
|
766800700a | ||
|
|
7e7823118e | ||
|
|
784a5818c8 | ||
|
|
75b95995a9 | ||
|
|
5941f0674a | ||
|
|
155a97c25d | ||
|
|
1fd0ebea9b | ||
|
|
988f61718c | ||
|
|
3d9a63a1f0 | ||
|
|
6c92ec314d | ||
|
|
132a01c27e | ||
|
|
b91f28d7d9 | ||
|
|
339840c14a | ||
|
|
a368975959 | ||
|
|
102e7610be | ||
|
|
e78c4d3bde | ||
|
|
1925efc2ec | ||
|
|
9562a86038 | ||
|
|
21d4f3d101 | ||
|
|
38ff62fddc | ||
|
|
26070d5b3e | ||
|
|
03d98ee256 | ||
|
|
dc28677086 | ||
|
|
54f4b397b6 | ||
|
|
96bcb6cd8e | ||
|
|
491b674e70 | ||
|
|
fdbee86d5e | ||
|
|
5b0d54e74d | ||
|
|
64754d1039 | ||
|
|
23c42def4f | ||
|
|
87a720dd58 | ||
|
|
10b78322a3 | ||
|
|
841a4d2227 | ||
|
|
3cacd74e12 | ||
|
|
71d4c6c42f | ||
|
|
75b05b9b14 | ||
|
|
daa8b9c798 | ||
|
|
bef14bd009 | ||
|
|
36d695118e | ||
|
|
4c3f42e264 | ||
|
|
3992054560 | ||
|
|
db8614999a | ||
|
|
bc2bd4510a | ||
|
|
5eca6ecb05 | ||
|
|
ce30e128e5 | ||
|
|
dedd67780b | ||
|
|
d8d954bbe8 | ||
|
|
bddf180575 | ||
|
|
9b78f766e8 | ||
|
|
e2e464d32c | ||
|
|
40b483357f | ||
|
|
f0efa4697e | ||
|
|
6d92d5fe21 | ||
|
|
c3e8e8b1fe | ||
|
|
a444a10bd0 | ||
|
|
d3551aa9cc | ||
|
|
4a2d1df384 | ||
|
|
a3c64c1d1e | ||
|
|
bbe8f34e72 | ||
|
|
02aa969d6c | ||
|
|
4da2a4f10f | ||
|
|
336e825e3a | ||
|
|
b42e51e300 | ||
|
|
6b1bfe7b17 | ||
|
|
6219f4bd90 | ||
|
|
3076c52adb | ||
|
|
933855a233 | ||
|
|
ea698259d5 | ||
|
|
aefa003355 | ||
|
|
e3b7472239 | ||
|
|
a3362c7ea4 | ||
|
|
66c45dc83b | ||
|
|
f7c33ee333 | ||
|
|
42160a08fe | ||
|
|
925a02dac3 | ||
|
|
7108fe99ec | ||
|
|
69203f3e23 | ||
|
|
4d2d21e101 | ||
|
|
8de727932d | ||
|
|
188fa6a04a | ||
|
|
0330a0e5cd | ||
|
|
0ad2d61742 | ||
|
|
f155b9f37e | ||
|
|
3a44bcb854 | ||
|
|
58335b3e59 | ||
|
|
657a74a5c2 | ||
|
|
f3112d4ba6 | ||
|
|
172b596cf1 | ||
|
|
b258581f47 | ||
|
|
60d5b3cadd | ||
|
|
f21ee40baf | ||
|
|
adf4ab1a35 | ||
|
|
83eeedc008 | ||
|
|
57cb1b5aa0 | ||
|
|
5e31b95bb5 | ||
|
|
80a1de60e6 | ||
|
|
2408f1530f | ||
|
|
23519913f2 | ||
|
|
7955ac6a46 | ||
|
|
dbbfdaf918 | ||
|
|
8e7207d975 | ||
|
|
c04f9e392a | ||
|
|
46bd617581 | ||
|
|
7617aeadda | ||
|
|
e009d05a90 | ||
|
|
f0063ba089 | ||
|
|
b08d945d7c | ||
|
|
abff0cf211 | ||
|
|
f52b3391d1 | ||
|
|
cae33a22e6 | ||
|
|
3dc263d6d7 | ||
|
|
8eb5fed92f | ||
|
|
ab1b3b4863 | ||
|
|
ab585fd699 | ||
|
|
91a1ee956c | ||
|
|
f8ad90c845 | ||
|
|
37a78bd1c3 | ||
|
|
18e3ca77de | ||
|
|
185baf6bc9 | ||
|
|
546a5849f3 | ||
|
|
2cc81219b1 | ||
|
|
1f862027af | ||
|
|
02643fb6bb | ||
|
|
e167b85d3b | ||
|
|
c0b6124d94 | ||
|
|
113b5ecbe0 | ||
|
|
4daf927a72 | ||
|
|
1218f18af9 | ||
|
|
7ca143d352 | ||
|
|
428010880e | ||
|
|
dc81740bda | ||
|
|
f7dc54b652 | ||
|
|
c0ab254acf | ||
|
|
5c4536c591 | ||
|
|
d48e82be7c | ||
|
|
bc4b14c83d | ||
|
|
6b9b437a5a | ||
|
|
d6566e5dfb | ||
|
|
6b28488fbd | ||
|
|
1a02511afe | ||
|
|
757ea5b63c | ||
|
|
24add0dce4 | ||
|
|
299c6478c2 | ||
|
|
ddd3d608fa | ||
|
|
d5b0a47d52 | ||
|
|
ca0a550f12 | ||
|
|
94084674c0 | ||
|
|
9aa56191f9 | ||
|
|
a34949f15c | ||
|
|
cbcc113e41 | ||
|
|
6f0b0e0c32 | ||
|
|
098a9c411a | ||
|
|
d1dd56550f | ||
|
|
59f328b804 | ||
|
|
783c49db37 | ||
|
|
015f02c077 | ||
|
|
e0833e86d9 | ||
|
|
f89f5f8469 | ||
|
|
0395ce8d1a | ||
|
|
ef87d031ed | ||
|
|
ba875da9b5 | ||
|
|
8f7e79862b | ||
|
|
9124ffabf1 | ||
|
|
c8914f90db | ||
|
|
7c43a4b0b7 | ||
|
|
e386ae0daf | ||
|
|
b5346668f8 | ||
|
|
7002e4cd09 | ||
|
|
ecfd550a95 | ||
|
|
5b46152945 | ||
|
|
88e076a283 | ||
|
|
0ed7e10379 | ||
|
|
21ee1e2c6c | ||
|
|
09f8233bc6 | ||
|
|
d5147f1935 | ||
|
|
4689479745 | ||
|
|
2be900745f | ||
|
|
baee322cd7 | ||
|
|
28fe63dbdb | ||
|
|
7718f62477 | ||
|
|
df0994dad3 | ||
|
|
dca43853fb | ||
|
|
ea8e31346c | ||
|
|
4aab3b686c | ||
|
|
ed495b3800 | ||
|
|
4025f223c4 | ||
|
|
121200cfad | ||
|
|
90e0ce4c48 | ||
|
|
ac0b442b43 | ||
|
|
af7021fac8 | ||
|
|
5468625212 | ||
|
|
345c1bf715 | ||
|
|
52d47a9786 | ||
|
|
2ba79a8508 | ||
|
|
ce7769472d | ||
|
|
eb5eea2625 | ||
|
|
d965dc3c07 | ||
|
|
040b3c371c | ||
|
|
c7ba8e54be | ||
|
|
33b1a5426c | ||
|
|
91197c0334 | ||
|
|
e075169dd4 | ||
|
|
225444b1b3 | ||
|
|
586ac9dcb9 | ||
|
|
c3be5311e8 | ||
|
|
a8c4d0a9c4 | ||
|
|
bfeded67ee | ||
|
|
dde0d25b77 | ||
|
|
296a34cd6e | ||
|
|
44cea68e83 | ||
|
|
790bf21166 | ||
|
|
5d17e264e6 | ||
|
|
7ef1e99f66 | ||
|
|
3acbacbdb3 | ||
|
|
5fd7062972 | ||
|
|
7a6695f6c4 | ||
|
|
8ac786581b | ||
|
|
86a8f69273 | ||
|
|
95b16cd805 | ||
|
|
3996b9a6b8 | ||
|
|
e286dea650 | ||
|
|
5e27e48129 | ||
|
|
95abb54dc8 | ||
|
|
dbd3d3c266 | ||
|
|
e5b9c841cd | ||
|
|
ad7acee64d | ||
|
|
9ce6674070 | ||
|
|
25edb60f2c | ||
|
|
b5a8d8b804 |
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
fixes:
|
||||||
|
- "github.com/9seconds/mtg/v2/::"
|
||||||
+7
-1
@@ -1,3 +1,9 @@
|
|||||||
version.go
|
|
||||||
vendor/
|
vendor/
|
||||||
tags
|
tags
|
||||||
|
.github/
|
||||||
|
.bin/
|
||||||
|
*.md
|
||||||
|
mtg
|
||||||
|
.golangci.toml
|
||||||
|
.gitignore
|
||||||
|
run.sh
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# git config merge.theirs.name "Always accept theirs"
|
||||||
|
# git config merge.theirs.driver "cp %B %A"
|
||||||
|
default.pgo binary merge=theirs
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
checks: read
|
||||||
|
contents: read
|
||||||
|
deployments: read
|
||||||
|
issues: read
|
||||||
|
discussions: read
|
||||||
|
pull-requests: read
|
||||||
|
repository-projects: read
|
||||||
|
security-events: read
|
||||||
|
statuses: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- stable
|
||||||
|
- v1
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
- released
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Cache Go modules and build
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: mise tasks run covtest
|
||||||
|
|
||||||
|
- name: Collect coverage
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
files: ./coverage.txt
|
||||||
|
|
||||||
|
fuzz:
|
||||||
|
name: Fuzzing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Cache Go modules and build
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Run fuzzing
|
||||||
|
run: mise tasks run 'test:fuzz:*'
|
||||||
|
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Cache Go modules and build
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: mise tasks run lint
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
name: Build release artifacts
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: jdx/mise-action@v3
|
||||||
|
name: Install mise
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-gomod-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gomod-
|
||||||
|
|
||||||
|
- name: Cache cross-compilation build
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-${{ hashFiles('**/*.go') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-
|
||||||
|
${{ runner.os }}-goreleaser-
|
||||||
|
|
||||||
|
- name: Run release
|
||||||
|
run: mise tasks run release
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Get Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
nineseconds/mtg
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }}
|
||||||
|
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Setup BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
pull: true
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions
|
||||||
|
# https://github.com/github/codeql-action/issues/572
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- stable
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '24 20 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
name: Vulnerability checks
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
checks: read
|
||||||
|
contents: read
|
||||||
|
deployments: read
|
||||||
|
issues: read
|
||||||
|
discussions: read
|
||||||
|
pull-requests: read
|
||||||
|
repository-projects: read
|
||||||
|
security-events: read
|
||||||
|
statuses: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule: # daily at 10:22 UTC
|
||||||
|
- cron: '22 10 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vuln:
|
||||||
|
name: Test vulnerabilities
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Check for vulnerabilities
|
||||||
|
run: |
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||||
+4
-3
@@ -6,6 +6,7 @@
|
|||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
mtg
|
mtg
|
||||||
vendor/
|
coverage.txt
|
||||||
version.go
|
dist/
|
||||||
ccbuilds/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
+17
-9
@@ -1,13 +1,21 @@
|
|||||||
|
# https://golangci-lint.run/docs/configuration/file/
|
||||||
|
|
||||||
|
version = '2'
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
concurrency = 4
|
concurrency = 4
|
||||||
deadline = "2m"
|
|
||||||
tests = true
|
tests = true
|
||||||
skip-dirs = ["vendor"]
|
|
||||||
skip-files = ["version.go"]
|
|
||||||
|
|
||||||
[output]
|
# [linters]
|
||||||
format = "colored-line-number"
|
# enable-all = true
|
||||||
|
# disable = [
|
||||||
[linters]
|
# "containedctx",
|
||||||
enable-all = true
|
# "exhaustivestruct",
|
||||||
disable = ["gochecknoglobals", "gomnd"]
|
# "exhaustruct",
|
||||||
|
# "gas",
|
||||||
|
# "gochecknoglobals",
|
||||||
|
# "goerr113",
|
||||||
|
# "ireturn",
|
||||||
|
# "thelper",
|
||||||
|
# "varnamelen",
|
||||||
|
# ]
|
||||||
|
|||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
project_name: mtg
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: default
|
||||||
|
binary: '{{ .ProjectName }}'
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- freebsd
|
||||||
|
- linux
|
||||||
|
- netbsd
|
||||||
|
- openbsd
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
goarm:
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags: -s -w -X main.version={{ .Version }}
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
|
- goos: netbsd
|
||||||
|
goarch: arm64
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: arm64
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
- id: mips
|
||||||
|
binary: '{{ .ProjectName }}'
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- mips
|
||||||
|
- mipsle
|
||||||
|
gomips:
|
||||||
|
- softfloat
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags: -s -w -X main.version={{ .Version }}
|
||||||
|
- id: arm64-v9
|
||||||
|
binary: '{{ .ProjectName }}'
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- arm64
|
||||||
|
goarm64:
|
||||||
|
- v9.0
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags: -s -w -X main.version={{ .Version }}
|
||||||
|
- id: amd64-v3
|
||||||
|
binary: '{{ .ProjectName }}'
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- freebsd
|
||||||
|
- linux
|
||||||
|
- netbsd
|
||||||
|
- openbsd
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
goamd64:
|
||||||
|
- v3
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags: -s -w -X main.version={{ .Version }}
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: default
|
||||||
|
ids:
|
||||||
|
- default
|
||||||
|
- mips
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||||
|
formats:
|
||||||
|
- tar.gz
|
||||||
|
wrap_in_directory: true
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- SECURITY.md
|
||||||
|
- BEST_PRACTICES.md
|
||||||
|
- example.config.toml
|
||||||
|
- id: optimized
|
||||||
|
ids:
|
||||||
|
- arm64-v9
|
||||||
|
- amd64-v3
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm64 }}-{{ .Arm64 }}{{ end }}{{ if .Amd64 }}-{{ .Amd64 }}{{ end }}'
|
||||||
|
formats:
|
||||||
|
- tar.gz
|
||||||
|
wrap_in_directory: true
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- SECURITY.md
|
||||||
|
- BEST_PRACTICES.md
|
||||||
|
- example.config.toml
|
||||||
|
|
||||||
|
gomod:
|
||||||
|
proxy: true
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
version_template: '{{ .Version }}'
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
|
||||||
|
|
||||||
|
source:
|
||||||
|
enabled: true
|
||||||
|
name_template: '{{ .ProjectName }}-sources'
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
[tools]
|
||||||
|
"go:golang.org/x/pkgsite/cmd/pkgsite" = "latest"
|
||||||
|
"go:golang.org/x/tools/gopls" = "latest"
|
||||||
|
"go:golang.org/x/vuln/cmd/govulncheck" = "latest"
|
||||||
|
"go:mvdan.cc/gofumpt" = "latest"
|
||||||
|
go = "latest"
|
||||||
|
golangci-lint = "latest"
|
||||||
|
goreleaser = "latest"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
fuzzflags = "-fuzztime=120s"
|
||||||
|
|
||||||
|
[tasks.build]
|
||||||
|
description = "Build binary"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum"]
|
||||||
|
outputs = ["mtg"]
|
||||||
|
run = "go build"
|
||||||
|
|
||||||
|
[tasks."build:prof"]
|
||||||
|
description = "Build binary with profiling enabled"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum"]
|
||||||
|
outputs = ["mtg"]
|
||||||
|
run = "go build -tags prof"
|
||||||
|
|
||||||
|
[tasks.update]
|
||||||
|
description = "Update dependencies"
|
||||||
|
run = [
|
||||||
|
"go get -u",
|
||||||
|
"go mod tidy -go=1.26"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.lint]
|
||||||
|
description = "Run linter"
|
||||||
|
run = "golangci-lint run"
|
||||||
|
|
||||||
|
[tasks.vuln]
|
||||||
|
description = "Test for vulnerabilities"
|
||||||
|
run = "govulncheck ./..."
|
||||||
|
|
||||||
|
[tasks.test]
|
||||||
|
description = "Run tests"
|
||||||
|
run = "go test -v -race ./..."
|
||||||
|
|
||||||
|
[tasks.covtest]
|
||||||
|
description = "Run tests with code coverage"
|
||||||
|
run = "go test -coverprofile=coverage.txt -covermode=atomic -count=2 -race -v ./..."
|
||||||
|
|
||||||
|
[tasks.test-all]
|
||||||
|
description = "Run all tests"
|
||||||
|
depends = [
|
||||||
|
"test",
|
||||||
|
"test:fuzz:*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."test:fuzz:client-hello"]
|
||||||
|
description = "Run fuzzy test for ClientHello"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:client-handshake"]
|
||||||
|
description = "Run fuzzy test for ClientHandshake"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/internal/obfuscation"
|
||||||
|
|
||||||
|
[tasks."test:fuzz:server-handshake-frame"]
|
||||||
|
description = "Run fuzzy test for GenerateHandshakeFrame"
|
||||||
|
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/internal/obfuscation"
|
||||||
|
|
||||||
|
[tasks.static]
|
||||||
|
description = "Build static binary"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum"]
|
||||||
|
outputs = ["mtg"]
|
||||||
|
run = """
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)"
|
||||||
|
go build \
|
||||||
|
-trimpath \
|
||||||
|
-mod=readonly \
|
||||||
|
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
|
||||||
|
-a \
|
||||||
|
-tags netgo
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.release]
|
||||||
|
description = "Create release tarballs"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum", ".goreleaser.yml"]
|
||||||
|
run = [
|
||||||
|
"goreleaser --snapshot --clean",
|
||||||
|
"find dist -depth 1 -type d | xargs -r rm -r",
|
||||||
|
"rm ./dist/config.yaml"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.image]
|
||||||
|
description = "Build docker image"
|
||||||
|
sources = ["**/*.go", "go.mod", "go.sum", "Dockerfile"]
|
||||||
|
run = "docker buildx build --pull -t mtg ."
|
||||||
|
|
||||||
|
[tasks.docs]
|
||||||
|
description = "Run doc server"
|
||||||
|
run = "pkgsite -http 0.0.0.0:10000"
|
||||||
|
|
||||||
|
[tasks.fmt]
|
||||||
|
description = "Reformat source code"
|
||||||
|
sources = ["**/*.go"]
|
||||||
|
run = "gofumpt -w --extra ."
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.13.x
|
|
||||||
- master
|
|
||||||
|
|
||||||
before_script: make prepare
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make all
|
|
||||||
- make lint
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- go: master
|
|
||||||
- go: 1.14.x # because of golangci-lint
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Best practices
|
||||||
|
|
||||||
|
This is unfortunate, but since 2018 many things were changed. Most of them
|
||||||
|
became way worse. Previous iterations of censorship systems were very dumb,
|
||||||
|
DPI were primitive and filtered very obvious things. Nowadays they are
|
||||||
|
way more intelligent and it is very naive to treat them frivolously.
|
||||||
|
|
||||||
|
In 2026 is not enough to pretend that your mtg installation is a Microsoft
|
||||||
|
website that sits in Amsterdam Digital Ocean location. Now your installation
|
||||||
|
has to be a website that is mtg in disguise. Yes, it requires a bit more effort
|
||||||
|
but this effort is probably less than rotating proxies each other day.
|
||||||
|
|
||||||
|
mtproto traffic, even with FakeTLS, has its specifics that are probably
|
||||||
|
very well known by DPI systems. These specifics are not something unique but
|
||||||
|
could mark an IP address as suspicious. Now let's think:
|
||||||
|
|
||||||
|
1. You have a proxy in Amsterdam Digital Ocean that tells it is microsoft.com
|
||||||
|
how hard could it be to find out that this is probably fake? 1 or probably 2
|
||||||
|
DNS queries for `microsoft.com`? In case of some CDN, there are ECS-powered
|
||||||
|
resolvers that are very capable to return results from POV of some subnets.
|
||||||
|
If censor sees no relevant results, will they be afraid to block IP?
|
||||||
|
2. You have a proxy in Amsterdam Digital Ocean that tells it is a website from
|
||||||
|
the same public subnet. But not the same. Would it be hard to make these DNS
|
||||||
|
queries and ban IP?
|
||||||
|
|
||||||
|
The correct way of having this proxy is following:
|
||||||
|
|
||||||
|
1. Register a domain name
|
||||||
|
2. Get some VPS, probably in your domestic location
|
||||||
|
3. Set that domain name from a step 1 to IP address of that VPS
|
||||||
|
4. Generate a couple of HTML pages by LLMs or even copy them from elsewhere
|
||||||
|
5. Set some webserver and issue TLS certificates with Let's Encrypt or any other
|
||||||
|
name
|
||||||
|
6. Set mtg before this webserver.
|
||||||
|
7. Use sing-box or anything like that to provide local socks5 interface and
|
||||||
|
have VPNized uplinks
|
||||||
|
8. Set up mtg to use socks5 from a 7 step.
|
||||||
|
|
||||||
|
In that case you will get a match of DNS and SNI in requests. As a side effect,
|
||||||
|
your proxy will work with XTLS and its friends: XTLS in sniff mode ignores
|
||||||
|
IP address a client wants to connect to. Instead, it reads SNI and connect
|
||||||
|
to resolved address: a clever idea if user does not have a trustworthy DNS
|
||||||
|
set up.
|
||||||
|
|
||||||
|
Yes, this is much longer that usual technique, and requires more effort. But
|
||||||
|
this is could probably be very well automated to some reasonable extent.
|
||||||
|
|
||||||
|
Unfortunately, this is a best practice right now.
|
||||||
|
|
||||||
|
Do not also forget about other implementation, like
|
||||||
|
[telemt](https://github.com/telemt/telemt). Try everything. Use VPNs. It does
|
||||||
|
not really matter which project you are going to use as long it helps you to
|
||||||
|
stay connected.
|
||||||
|
|
||||||
|
_March 2026._
|
||||||
+42
-22
@@ -1,23 +1,45 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# BUILD STAGE
|
# BUILD STAGE
|
||||||
|
|
||||||
FROM golang:1.14-alpine
|
FROM golang:1.26-alpine AS build
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
|
# this is done for backward compatibility: before that we mounted a config
|
||||||
|
# into /config.toml. Some application allow mounting directories only,
|
||||||
|
# so it makes problems. So, instead we are going to do 2 steps:
|
||||||
|
# 1. Create /config/config.toml as a symlink to /config.toml
|
||||||
|
# 2. Force /mtg to use /config/config.toml
|
||||||
|
#
|
||||||
|
# it helps in both ways: users with directories could use /config directory
|
||||||
|
# and overlap a symlink by their bind mount. Old users could continue using
|
||||||
|
# /config.toml as a real config.
|
||||||
|
RUN set -x \
|
||||||
|
&& mkdir -p /config \
|
||||||
|
&& ln -sv /config.toml /config/config.toml
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apk \
|
||||||
|
set -x \
|
||||||
|
&& apk --update add \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
git
|
||||||
|
|
||||||
|
COPY go.mod go.sum /app/
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& apk --no-cache --update add \
|
&& version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always 2>/dev/null || echo dev)" \
|
||||||
bash \
|
&& go build \
|
||||||
ca-certificates \
|
-trimpath \
|
||||||
curl \
|
-mod=readonly \
|
||||||
git \
|
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
|
||||||
make \
|
-a \
|
||||||
upx
|
-tags netgo
|
||||||
|
|
||||||
COPY . /go/src/github.com/9seconds/mtg/
|
|
||||||
|
|
||||||
RUN set -x \
|
|
||||||
&& cd /go/src/github.com/9seconds/mtg \
|
|
||||||
&& make -j 4 static \
|
|
||||||
&& upx --ultra-brute -qq ./mtg
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -26,11 +48,9 @@ RUN set -x \
|
|||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
ENTRYPOINT ["/mtg"]
|
ENTRYPOINT ["/mtg"]
|
||||||
ENV MTG_IP=0.0.0.0 \
|
CMD ["run", "/config/config.toml"]
|
||||||
MTG_PORT=3128 \
|
|
||||||
MTG_STATS_IP=0.0.0.0 \
|
|
||||||
MTG_STATS_PORT=3129
|
|
||||||
EXPOSE 3128 3129
|
|
||||||
|
|
||||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
COPY --from=0 /go/src/github.com/9seconds/mtg/mtg /mtg
|
COPY --from=build /app/mtg /mtg
|
||||||
|
COPY --from=build /app/example.config.toml /config.toml
|
||||||
|
COPY --from=build /config /config
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
IMAGE_NAME := mtg
|
|
||||||
APP_NAME := $(IMAGE_NAME)
|
|
||||||
|
|
||||||
CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}")
|
|
||||||
|
|
||||||
GOLANGCI_LINT_VERSION := v1.24.0
|
|
||||||
|
|
||||||
VERSION_GO := $(shell go version)
|
|
||||||
VERSION_DATE := $(shell date -Ru)
|
|
||||||
VERSION_TAG := $(shell git describe --tags --always)
|
|
||||||
COMMON_BUILD_FLAGS := -ldflags="-s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
|
|
||||||
|
|
||||||
MOD_ON := env GO111MODULE=on
|
|
||||||
MOD_OFF := env GO111MODULE=auto
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
$(APP_NAME):
|
|
||||||
@$(MOD_ON) go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
|
||||||
|
|
||||||
static-$(APP_NAME):
|
|
||||||
@$(MOD_ON) env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
|
|
||||||
|
|
||||||
$(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
|
|
||||||
$(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
|
|
||||||
$(APP_NAME)-%: ccbuilds
|
|
||||||
@$(MOD_ON) env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
|
|
||||||
go build \
|
|
||||||
$(COMMON_BUILD_FLAGS) \
|
|
||||||
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
|
|
||||||
|
|
||||||
ccbuilds:
|
|
||||||
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
|
|
||||||
|
|
||||||
vendor: go.mod go.sum
|
|
||||||
@$(MOD_ON) go mod vendor
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: $(APP_NAME)
|
|
||||||
|
|
||||||
.PHONY: static
|
|
||||||
static: static-$(APP_NAME)
|
|
||||||
|
|
||||||
.PHONY: crosscompile
|
|
||||||
crosscompile: $(CC_BINARIES)
|
|
||||||
|
|
||||||
.PHONY: crosscompile-dir
|
|
||||||
crosscompile-dir:
|
|
||||||
@rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)"
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: vendor
|
|
||||||
@$(MOD_OFF) golangci-lint run
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
@git clean -xfd && \
|
|
||||||
git reset --hard >/dev/null && \
|
|
||||||
git submodule foreach --recursive sh -c 'git clean -xfd && git reset --hard' >/dev/null
|
|
||||||
|
|
||||||
.PHONY: docker
|
|
||||||
docker:
|
|
||||||
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
|
|
||||||
|
|
||||||
.PHONY: prepare
|
|
||||||
prepare: install-lint
|
|
||||||
|
|
||||||
.PHONY: install-lint
|
|
||||||
install-lint:
|
|
||||||
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
|
|
||||||
| $(MOD_OFF) bash -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION)
|
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
# mtg
|
# mtg
|
||||||
|
|
||||||
Bullshit-free MTPROTO proxy for Telegram
|
Highly-opinionated (ex-bullshit-free) MTPROTO proxy for
|
||||||
|
[Telegram](https://telegram.org/).
|
||||||
|
|
||||||
[](https://travis-ci.org/9seconds/mtg)
|
[](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/9seconds/mtg)
|
[](https://codecov.io/gh/9seconds/mtg)
|
||||||
[](https://hub.docker.com/r/nineseconds/mtg/)
|
[](https://pkg.go.dev/github.com/9seconds/mtg/v2)
|
||||||
|
|
||||||
**Please see a guide on upgrading to 1.0 at the end of this README.**
|
**If you use v1.0 or upgrade broke you proxy, please read the chapter
|
||||||
|
[Version 2](#version-2)**
|
||||||
|
|
||||||
# Rationale
|
If you want to have a proxy that _supports adtag_ (possibility to promote a
|
||||||
|
channel with a special Telegram bot), I recommend to use
|
||||||
|
[telemt](https://github.com/telemt/telemt) project. v1 of mtg supports it
|
||||||
|
but I do not see any reasonable point of using it: adtag requires communication
|
||||||
|
via a fragile set of middle proxies, requires complex setup that must expose
|
||||||
|
a public IPs, has lower bandwidth and latency.
|
||||||
|
|
||||||
|
mtg idea is simple: minimal unbloated proxy that can handle a reasonable scale
|
||||||
|
~10-20k simultaneous connections, has no user management, but ticks all
|
||||||
|
checkboxes related to its main intent: provide a way to use Telegram.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
There are several available proxies for Telegram MTPROTO available. Here
|
There are several available proxies for Telegram MTPROTO available. Here
|
||||||
are the most notable:
|
are the most notable:
|
||||||
@@ -16,269 +29,563 @@ are the most notable:
|
|||||||
* [Official](https://github.com/TelegramMessenger/MTProxy)
|
* [Official](https://github.com/TelegramMessenger/MTProxy)
|
||||||
* [Python](https://github.com/alexbers/mtprotoproxy)
|
* [Python](https://github.com/alexbers/mtprotoproxy)
|
||||||
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
* [Erlang](https://github.com/seriyps/mtproto_proxy)
|
||||||
|
* [Teleproxy (C)](https://github.com/teleproxy/teleproxy)
|
||||||
|
* [mtproto.zig (Zig)](https://github.com/sleep3r/mtproto.zig)
|
||||||
|
* [Telemt (Rust)](https://github.com/telemt/telemt)
|
||||||
|
|
||||||
Almost all of them follow the way how official proxy was built. This
|
You can use any of these. They work great and all implementations have
|
||||||
includes support of multiple secrets, support of promoted channels, etc.
|
feature parity now. This includes support of adtag, replay attack
|
||||||
|
protection, domain fronting, faketls, and so on. mtg has a similar
|
||||||
|
goal: to give a possibility to connect to Telegram in a restricted,
|
||||||
|
censored environment. But it does it slightly differently in details
|
||||||
|
that probably matter.
|
||||||
|
|
||||||
mtg is an implementation in golang which is intended to be:
|
* **Domain fronting**
|
||||||
|
|
||||||
|
For years mtg supports domain fronting. This technique means that it fallbacks
|
||||||
|
to accessing a real website in case if request fails. It could fail by many
|
||||||
|
reasons: anti-replay protection, accidental access to the webserver or
|
||||||
|
stale request. Anyway, if mtg rejects this request, it does not break a
|
||||||
|
connection. It connects to the websites and replicates everything that client
|
||||||
|
has sent, and simply proxies it back as is. Users will see a response from
|
||||||
|
the real website, _byte-to-byte identical_ to the response of the real netloc.
|
||||||
|
|
||||||
|
* **Doppelganger**
|
||||||
|
|
||||||
|
mtg also is a doppelganger of the website it fronts. Sure, with domain fronting
|
||||||
|
users will see replies of the real website in case if something will go wrong.
|
||||||
|
But what about such cases when _everything is fine_?
|
||||||
|
|
||||||
|
In that case mtg mimics TLS connection statistical characteristics as close as
|
||||||
|
possible. Different application have different statistics of their patterns.
|
||||||
|
Big CDN steadily pumping the data, small websites burst with short easily
|
||||||
|
compressiable chunks of traffic.
|
||||||
|
|
||||||
|
mtg artificially emulates those delays to be statistically indistinguishable
|
||||||
|
from the real website even if it covers connection of the very specific app.
|
||||||
|
It also follows 2 most common patterns of traffic chunking, so censors
|
||||||
|
will have to put more resources to find out that we have Telegram here
|
||||||
|
but not a hookah webshop served by nginx.
|
||||||
|
|
||||||
|
* **Resource-efficient**
|
||||||
|
|
||||||
|
It has to be resource-efficient. It does not mean that you will see
|
||||||
|
the smallest memory usage. It means that it will try to use allocated
|
||||||
|
resources in zero-waste mode, reusing as much memory as possible and
|
||||||
|
so on.
|
||||||
|
|
||||||
* **Lightweight**
|
|
||||||
It has to consume as few resources as possible but not by losing
|
|
||||||
maintainability.
|
|
||||||
* **Easily deployable**
|
* **Easily deployable**
|
||||||
|
|
||||||
I strongly believe that Telegram proxies should follow the way of
|
I strongly believe that Telegram proxies should follow the way of
|
||||||
ShadowSocks: promoted channels is a strange way of doing business
|
[ShadowSocks](https://shadowsocks.org): promoted channels is a strange
|
||||||
I suppose. I think the only viable way is to have a proxy with
|
way of doing business I suppose. I think the only viable way is to
|
||||||
minimum configuration which should work everywhere.
|
have a proxy that can be restored anywhere easily.
|
||||||
|
|
||||||
|
* **Supports proxy protocol v1/v2**
|
||||||
|
|
||||||
|
This makes integration with loadbalancers like HAProxy and ELB a first class
|
||||||
|
citizen by supporting their
|
||||||
|
[commuication protocols](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt).
|
||||||
|
|
||||||
* **A single secret**
|
* **A single secret**
|
||||||
I think that multiple secrets solve no problems and just complexify
|
|
||||||
|
I think that multiple secrets solve no problems and just complex
|
||||||
software. I also believe that in the case of throwout proxies, this
|
software. I also believe that in the case of throwout proxies, this
|
||||||
feature is a useless luxury.
|
the feature is a useless luxury.
|
||||||
* **Minimum docker image size**
|
|
||||||
Official image is less than 3.5 megabytes. Literally.
|
This is very controversial topic. Please read [rationale (in russian)](https://github.com/9seconds/mtg/issues/376#issuecomment-4118726699)
|
||||||
|
and use [mtg-multi](https://github.com/dolonet/mtg-multi) fork if you are disagree with.
|
||||||
|
|
||||||
|
* **No adtag support**
|
||||||
|
|
||||||
|
Please read [Version 2](#version-2) chapter.
|
||||||
|
|
||||||
* **No management WebUI**
|
* **No management WebUI**
|
||||||
|
|
||||||
This is an implementation of a simple lightweight proxy. I won't do that.
|
This is an implementation of a simple lightweight proxy. I won't do that.
|
||||||
|
|
||||||
This proxy supports 2 modes of work: direct connection to Telegram and
|
* **Proxy chaining**
|
||||||
promoted channel mode. If you do not need promoted channels, I would
|
|
||||||
recommend you to go with direct mode: this way is more robust.
|
|
||||||
|
|
||||||
To run a proxy in direct mode, all you need to do is just provide a
|
mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
|
||||||
secret. If you do not provide ADTag as a second parameter, promoted
|
proxies. So, in theory, you can run this proxy as a frontend
|
||||||
channels mode won't be activated.
|
and route traffic via [v2ray](https://www.v2ray.com/),
|
||||||
|
[Gost](https://docs.ginuerzh.xyz/gost/),
|
||||||
|
[Trojan](https://trojan-gfw.github.io/trojan/), or any other project
|
||||||
|
you like.
|
||||||
|
|
||||||
To get promoted channel, please contact
|
* **Native blocklist support**
|
||||||
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
|
|
||||||
second parameter.
|
|
||||||
|
|
||||||
|
Previously, this was delegated to the [FireHOL](https://firehol.org/)
|
||||||
|
project or similar ones which track attacks and publish a list of
|
||||||
|
potentially dangerous IPs. mtg has native support of such blocklists.
|
||||||
|
|
||||||
# Source code organization
|
* **Can be used as a library**
|
||||||
|
|
||||||
There are 2 main branches:
|
mtg v2 was redesigned in a way so it can be embedded into your
|
||||||
|
software (written in Golang) with a minimum effort + you can replace
|
||||||
|
some parts with those you want.
|
||||||
|
|
||||||
1. `master` branch contains potentially unstable features
|
Please also to read about [best practices](https://github.com/9seconds/mtg/blob/master/BEST_PRACTICES.md).
|
||||||
2. `stable` branch contains stable version. Usually you want to use this branch.
|
|
||||||
|
|
||||||
# How to build
|
### Version 2
|
||||||
|
|
||||||
|
If you use version 1.x before, you are probably noticed some major
|
||||||
|
backward non-compatible details:
|
||||||
|
|
||||||
|
1. Configuration file
|
||||||
|
2. Removed support of adtag
|
||||||
|
|
||||||
|
For the configuration file, please check out the full example in this
|
||||||
|
repository. It has a lot of comments and most of the options are
|
||||||
|
optional. We do have only `secret` and `bind-to` sections mandatory.
|
||||||
|
Other sections in the example configuration file are filled with default
|
||||||
|
values.
|
||||||
|
|
||||||
|
Adtag support was removed completely. This was done to debloat mtg and
|
||||||
|
keep it simple and obvious. Hopefully, this goal is achieved and the
|
||||||
|
source code is clean and straightforward enough.
|
||||||
|
|
||||||
|
I always was quite skeptical about adtag. In my POV, a proxy as a fat
|
||||||
|
big connectivity point for hundreds of clients is an illusion. If you
|
||||||
|
work in a censored environment, the first thing that authority does is
|
||||||
|
IP blocking. For us, it means, those big proxies that can benefit from
|
||||||
|
having a pinned channel are going to be blocked in a minute.
|
||||||
|
|
||||||
|
Proxy has to be intimate. It has to be shared within a small group as
|
||||||
|
a family or maybe your college friends. It has to have a small number
|
||||||
|
of connections and never publicly announced its presence. It has to fly
|
||||||
|
under the radar. If the proxy is detected, you need to be able to give
|
||||||
|
a rebirth on a new IP address as soon as possible. I do no think that
|
||||||
|
having some special channel for such a use case makes any sense.
|
||||||
|
|
||||||
|
But other details like replay attack protection, domain fronting,
|
||||||
|
accurate FakeTLS implementation, IP blacklisting, and proxy
|
||||||
|
chaining matter here. If you work in censored perimeter like
|
||||||
|
[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
|
||||||
|
country, you probably want to have an MTPROTO proxy as
|
||||||
|
a frontend that transports traffic via cloaked tunnels
|
||||||
|
made by [Trojan](https://trojan-gfw.github.io/trojan/),
|
||||||
|
[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
|
||||||
|
or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
|
||||||
|
the support of chaining as a first-class citizen.
|
||||||
|
|
||||||
|
Yes, this is possible and doable with optional adtag support. But the
|
||||||
|
truth is that the MTPROTO proxy for Telegram is just a thing that either
|
||||||
|
work as a normal client (direct mode) or doing some RPC calls in [TL
|
||||||
|
language](https://core.telegram.org/mtproto/TL) (adtag support). I
|
||||||
|
understand the intention of the developers and I understand that they
|
||||||
|
were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
|
||||||
|
doing TON after that. Nothing is ideal. But for the proxy, it means that
|
||||||
|
source code is full of complex non-trivial code which is required only
|
||||||
|
to support a feature that we barely need.
|
||||||
|
|
||||||
|
So, to have a reasonable MTPROTO proxy, adtag support was removed. This
|
||||||
|
is a rare chance in my career where software v2 debloats a previous
|
||||||
|
version. It feels so good :)
|
||||||
|
|
||||||
|
### Version 1 and 2
|
||||||
|
|
||||||
|
I do continue to support both versions 1 and 2. But in a different mode.
|
||||||
|
|
||||||
|
Version 1 is now officially in maintenance mode. It means that I won't
|
||||||
|
make any new features or improvements there. You can consider a feature
|
||||||
|
freeze there. No bugs are going to be fixed there except for critical
|
||||||
|
ones. PRs are welcome though. The goal is to keep it working. It will
|
||||||
|
get some periodical updates like updates to the new Golang version of
|
||||||
|
dependencies version bump, but that's mostly it.
|
||||||
|
|
||||||
|
**If you want to have mtg with _adtag support_, please use version 1**.
|
||||||
|
|
||||||
|
Version 2 is going to have all my love, active support, bug fixing, etc.
|
||||||
|
It is under active development and maintenance.
|
||||||
|
|
||||||
|
This project has several main branches
|
||||||
|
|
||||||
|
1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
|
||||||
|
contains a bleeding edge. It may potentially have some features
|
||||||
|
which will break your source code.
|
||||||
|
2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
|
||||||
|
dumps of a master branch when we consider it 'stable'. This is a
|
||||||
|
branch you probably want to pick.
|
||||||
|
3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
|
||||||
|
of the v2.x version. In theory, it is the same as `master` but this
|
||||||
|
will change when we have v3.x.
|
||||||
|
4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Download mise
|
||||||
|
|
||||||
|
mtg uses [mise](https://mise.jdx.dev/) to maintain its development
|
||||||
|
dependencies + replaces a make for building things. Please
|
||||||
|
[install](https://mise.jdx.dev/getting-started.html) it first.
|
||||||
|
|
||||||
|
### Download a tool
|
||||||
|
|
||||||
|
#### Download binaries
|
||||||
|
|
||||||
|
Binaries can be downloaded from the release page. Also, you can download
|
||||||
|
docker image.
|
||||||
|
|
||||||
|
For the current version, please download like
|
||||||
|
|
||||||
```console
|
```console
|
||||||
make
|
docker pull nineseconds/mtg:2
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to build for another platform:
|
For version 1:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
make crosscompile
|
docker pull nineseconds/mtg:1
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to build Docker image (called `mtg`):
|
You may also check both [Docker
|
||||||
|
Hub](https://hub.docker.com/r/nineseconds/mtg/tags) and [Github
|
||||||
|
Registry](https://github.com/users/9seconds/packages/container/package/mtg).
|
||||||
|
Please do not choose `latest` or `stable` if you want to avoid
|
||||||
|
surprises. Always choose some version tag.
|
||||||
|
|
||||||
|
Also, if you have `go` installed, you can always download this tool with `go get`:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
make docker
|
go install github.com/9seconds/mtg/v2@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
# Docker image
|
#### Build from sources
|
||||||
|
|
||||||
Docker follows the same policy as the source code organization:
|
|
||||||
|
|
||||||
- `latest` mirrors the master branch
|
|
||||||
- `stable` mirrors the stable branch
|
|
||||||
- tags are for tagged releases
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:latest
|
git clone https://github.com/9seconds/mtg.git
|
||||||
|
cd mtg
|
||||||
|
mise install
|
||||||
|
mise tasks run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or for the docker image:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:stable
|
mise tasks run image
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generate secret
|
||||||
|
|
||||||
|
If you already have a secret in Base64 format or that, which starts with `ee`,
|
||||||
|
you can skip this chapter. Otherwise:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
docker pull nineseconds/mtg:0.10
|
$ mtg generate-secret google.com
|
||||||
|
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||||
```
|
```
|
||||||
|
|
||||||
# Ansible role
|
or
|
||||||
|
|
||||||
You can find unofficial Ansible role for mtg here: https://github.com/rlex/ansible-role-mtg
|
|
||||||
Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/ivansible/lin_mtproxy
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
|
|
||||||
To run this tool you need to configure as less as possible. Telegram
|
|
||||||
clients support 3 different secret types:
|
|
||||||
|
|
||||||
* Simple - basically, it is just a flow of frames ciphered by AES-CTR stream
|
|
||||||
cipher.
|
|
||||||
* Secured - the same stream as simple but with some random noise to prevent
|
|
||||||
statistical analysis of traffic flow.
|
|
||||||
* FakeTLS - this mode envelops telegram stream in TLS so it looks (in theory)
|
|
||||||
the same as any TLS1.3 traffic from DPI point of view.
|
|
||||||
|
|
||||||
If you do not have preferences, go with FakeTLS or at least secured.
|
|
||||||
Simple mode is a little bit naive and traffic flow can be easily
|
|
||||||
identified as Telegram one.
|
|
||||||
|
|
||||||
Unlike the rest of implementation, mtg is quite strict about the
|
|
||||||
execution mode: if you run a proxy instance with FakeTLS secret, you
|
|
||||||
can't connect to it with simple or secured clients. You can't connect
|
|
||||||
to the proxy with secured secret with FakeTLS key. It forces one mode
|
|
||||||
of working. So, unfortunately, there is no way how to connect to the
|
|
||||||
deployed proxy with another secret (if you know how to construct and
|
|
||||||
convert them). But at the same time, old clients can't connect so they
|
|
||||||
won't expose the type of the service.
|
|
||||||
|
|
||||||
First, you need to generate a secret:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg generate-secret simple
|
$ mtg generate-secret --hex google.com
|
||||||
52a493bdfb90eea55739eabff2d92a14
|
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
equivalent commands with docker:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg generate-secret secured
|
$ docker run --rm nineseconds/mtg:2 generate-secret google.com
|
||||||
ddf05fb7acb549be047a7c585116581418
|
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
|
||||||
|
|
||||||
|
$ docker run --rm nineseconds/mtg:2 generate-secret --hex google.com
|
||||||
|
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This secret is a keystone for a proxy and your password for a client.
|
||||||
|
You need to keep it secured.
|
||||||
|
|
||||||
|
We recommend choosing a hostname wisely. Here we have a _google.com_
|
||||||
|
but in reality, all providers can easily detect that this is not a
|
||||||
|
Google. Google has a list of networks it officially uses and your IP
|
||||||
|
address won't probably belong to it. It is a great idea to hide behind
|
||||||
|
some domain that has some relation to this IP address.
|
||||||
|
|
||||||
|
For example, you've bought a VPS from [Digital
|
||||||
|
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
|
||||||
|
generate a secret for _digitalocean.com_ then.
|
||||||
|
|
||||||
|
### Check configuration
|
||||||
|
|
||||||
|
There is a special command for secret verification:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mtg doctor /path/to/my/config.toml
|
||||||
|
Deprecated options
|
||||||
|
✅ All good
|
||||||
|
Time skewness
|
||||||
|
✅ Time drift is -607.048µs, but tolerate-time-skewness is 5s
|
||||||
|
Validate native network connectivity
|
||||||
|
✅ DC 1
|
||||||
|
✅ DC 2
|
||||||
|
✅ DC 3
|
||||||
|
✅ DC 4
|
||||||
|
✅ DC 5
|
||||||
|
✅ DC 203
|
||||||
|
Validate network connectivity with proxy socks5://127.0.0.1:1080
|
||||||
|
✅ DC 1
|
||||||
|
✅ DC 2
|
||||||
|
✅ DC 3
|
||||||
|
✅ DC 4
|
||||||
|
✅ DC 5
|
||||||
|
✅ DC 203
|
||||||
|
Validate fronting domain connectivity
|
||||||
|
✅ xx.xx.xx.xx:yyy is reachable
|
||||||
|
Validate SNI-DNS match
|
||||||
|
✅ IP address xx.xx.xx.xx matches secret hostname <REDACTED>
|
||||||
|
```
|
||||||
|
|
||||||
|
It aims to find out possible inconsistencies and problems with your
|
||||||
|
configuration. It makes sense to run it before executing any relevant commands.
|
||||||
|
|
||||||
|
### Simple run mode
|
||||||
|
|
||||||
|
mtg supports 2 modes: simple and normal. Simple mode allows starting
|
||||||
|
proxy with a small subset of configuration options you usually want to
|
||||||
|
modify. This is quite good for oneliners that you can copy-paste and do
|
||||||
|
not bother about external files whatsoever.
|
||||||
|
|
||||||
|
Let's take a look:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg generate-secret -c google.com tls
|
Usage: mtg simple-run <bind-to> <secret>
|
||||||
ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d
|
|
||||||
|
Run proxy without config file.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<bind-to> A host:port to bind proxy to.
|
||||||
|
<secret> Proxy secret.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help Show context-sensitive help.
|
||||||
|
-v, --version Print version.
|
||||||
|
|
||||||
|
-d, --debug Run in debug mode.
|
||||||
|
-c, --concurrency=8192 Max number of concurrent connection to proxy.
|
||||||
|
-b, --tcp-buffer="4KB" Size of TCP buffer to use.
|
||||||
|
-i, --prefer-ip="prefer-ipv6" IP preference. By default we prefer IPv6 with fallback to IPv4.
|
||||||
|
-p, --domain-fronting-port=443 A port to access for domain fronting.
|
||||||
|
-n, --doh-ip=1.1.1.1 IP address of DNS-over-HTTP to use.
|
||||||
|
-t, --timeout=10s Network timeout to use
|
||||||
|
-a, --antireplay-cache-size="1MB" A size of anti-replay cache to use.
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer docker:
|
So, if you want to startup a proxy with CLI only, you can do something like
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker run --rm nineseconds/mtg generate-secret tls -c bing.com
|
$ mtg simple-run -n 1.1.1.1 -t 30s -a 512kib 127.0.0.1:3128 7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t
|
||||||
eedf71035a8ed48a623d8e83e66aec4d0562696e672e636f6d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Antireplay cache
|
The rest of the configuration will be taken from default values. But
|
||||||
|
a simple run is fine if you do not have any special requirements or
|
||||||
|
granular tuning. If you want it, please checkout the configuration
|
||||||
|
files.
|
||||||
|
|
||||||
To prevent replay attacks, we have internal storage of first frames
|
### Prepare a configuration file
|
||||||
messages for connected clients. These frames are generated randomly
|
|
||||||
by design and we have the negligible possibility of duplication
|
|
||||||
(probability is 1/(2^64)) but it could be quite effective to prevent
|
|
||||||
replays.
|
|
||||||
|
|
||||||
It is possible to disable this cache. To do that, please explicitly set
|
Please checkout an example configuration file. All options except of
|
||||||
its size to 0.
|
`secret` and `bind-to` are optional. You can safely have this minimal
|
||||||
|
configuration file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
secret = "ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d"
|
||||||
|
bind-to = "0.0.0.0:443"
|
||||||
|
```
|
||||||
|
|
||||||
## FakeTLS
|
This is enough to run the whole application. All other
|
||||||
|
options already have sensible defaults for the app at almost any scale.
|
||||||
|
|
||||||
If you run this a proxy in faketls mode, this proxy will try to hide
|
Oh, the configuration is done in [TOML format](https://toml.io/en/).
|
||||||
itself cloaking a host provided as a part of the generated secret. It
|
|
||||||
means that if you cloak google.com then you can curl this proxy and
|
|
||||||
you'll get a google.com response back.
|
|
||||||
|
|
||||||
mtg proxies L3 traffic. In other words, only TCP, without interfering in
|
### Run a proxy
|
||||||
TLS, HTTP or any other high-level protocol.
|
|
||||||
|
|
||||||
|
Put a binary and a config into your webserver. Just for example,
|
||||||
|
a binary goes to `/usr/local/bin/mtg` and configuration to `/etc/mtg.toml`.
|
||||||
|
|
||||||
## Environment variables
|
Now you can create a systemd unit:
|
||||||
|
|
||||||
It is possible to configure this tool using environment variables. You
|
|
||||||
can configure any flag but not secret or adtag. Here is the list of
|
|
||||||
supported environment variables:
|
|
||||||
|
|
||||||
| Environment variable | Corresponding flags | Default value | Description |
|
|
||||||
|-------------------------------|------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `MTG_DEBUG` | `-d`, `--debug` | `false` | Run in debug mode. Usually, you need to run in this mode only if you develop this tool or its maintainer is asking you to provide logs with such verbosity. |
|
|
||||||
| `MTG_VERBOSE` | `-v`, `--verbose` | `false` | Run in verbose mode. This is way less chatty than debug mode. |
|
|
||||||
| `MTG_BIND` | `-b`, `--bind` | `0.0.0.0:3128` | Which host/port pair should we bind to (listen on). |
|
|
||||||
| `MTG_IPV4` | `-4`, `--public-ipv4` | [Autodetect](https://ifconfig.co) | IPv4 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv4 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
|
||||||
| `MTG_IPV6` | `-6`, `--public-ipv6` | [Autodetect](https://ifconfig.co) | IPv6 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv6 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. |
|
|
||||||
| `MTG_STATS_BIND` | `-t`, `--stats-bind` | `127.0.0.1:3129` | Which hist:port should we bind the internal statistics HTTP server (Prometheus). |
|
|
||||||
| `MTG_STATS_NAMESPACE` | `--stats-namespace` | `mtg` | Which namespace should be used for prometheus metrics. |
|
|
||||||
| `MTG_STATSD_ADDR` | `--statsd-addr` | | IP:host addresses of statsd service. No defaults, by defaults we do not send anything there. |
|
|
||||||
| `MTG_STATSD_PORT` | `--statsd-port` | `8125` | Which port should we use to work with statsd. |
|
|
||||||
| `MTG_STATSD_PREFIX` | `--statsd-prefix` | `mtg` | Which bucket prefix we should use. For example, if you set `mtg`, then metric `traffic.ingress` would be send as `mtg.traffic.ingress`. |
|
|
||||||
| `MTG_STATSD_TAGS_FORMAT` | `--statsd-tags-format` | | Which tags format we should use. By default, we are using default vanilla statsd tags format but if you want to send directly to InfluxDB or Datadog, please specify it there. Possible options are `influxdb` and `datadog`. |
|
|
||||||
| `MTG_STATSD_TAGS` | `--statsd-tags` | | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs. |
|
|
||||||
| `MTG_BUFFER_WRITE` | `-w`, `--write-buffer` | `32KB` | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram. |
|
|
||||||
| `MTG_BUFFER_READ` | `-r`, `--read-buffer` | `32KB` | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client. |
|
|
||||||
| `MTG_ANTIREPLAY_MAXSIZE` | `--anti-replay-max-size` | `128MB` | Max size of antireplay cache. |
|
|
||||||
| `MTG_CLOAK_PORT` | `--cloak-port` | `443` | Which port we should use to connect to cloaked host in FakeTLS mode. |
|
|
||||||
| `MTG_MULTIPLEX_PERCONNECTION` | `--multiplex-per-connection` | `50` | How many client connections can share a single Telegram connection in adtag mode |
|
|
||||||
| `MTG_NTP_SERVERS` | `--ntp-server` | default pool | A list of NTP servers to use. |
|
|
||||||
| `MTG_PREFER_DIRECT_IP` | `--prefer-ip` | `ipv6` | Which IP protocol to prefer if possible. Works mostly in direct mode. |
|
|
||||||
|
|
||||||
|
|
||||||
Usually you want to modify only read/write buffer sizes. If you feel
|
|
||||||
that proxy is slow, try to increase both sizes giving more priority to
|
|
||||||
read buffer.
|
|
||||||
|
|
||||||
Unfortunately, MTPROTO proxy protocol does not allow us to use splice
|
|
||||||
or any other neat tricks how to eliminate the need of copying data into
|
|
||||||
userspace.
|
|
||||||
|
|
||||||
# How to run the tool
|
|
||||||
|
|
||||||
Now run the tool:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg run <secret>
|
$ cat /etc/systemd/system/mtg.service
|
||||||
|
[Unit]
|
||||||
|
Description=mtg - MTProto proxy server
|
||||||
|
Documentation=https://github.com/9seconds/mtg
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
DynamicUser=true
|
||||||
|
LimitNOFILE=65536
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
$ sudo systemctl daemon-reload
|
||||||
|
$ sudo systemctl enable mtg
|
||||||
|
$ sudo systemctl start mtg
|
||||||
```
|
```
|
||||||
|
|
||||||
How to run the tool with ADTag:
|
or you can run a docker image
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ mtg run <secret> <adtag>
|
docker run -d -v $PWD/config.toml:/config.toml -p 443:3128 --name mtg-proxy --restart=unless-stopped nineseconds/mtg:2
|
||||||
```
|
```
|
||||||
|
|
||||||
This tool will listen on port 3128 by default with the given secret.
|
where _443_ is a host port (a port you want to connect to from a
|
||||||
|
client), and _3128_ is the one you have in your config in the `bind-to`
|
||||||
|
section.
|
||||||
|
|
||||||
|
### Access a proxy
|
||||||
|
|
||||||
# oneliner to run this proxy
|
Now you can generate some useful links:
|
||||||
|
|
||||||
Please ensure that docker is installed. After that just execute
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
curl -sfL --compressed https://raw.githubusercontent.com/9seconds/mtg/master/run.sh | bash
|
$ mtg access /etc/mtg.toml
|
||||||
|
{
|
||||||
|
"ipv4": {
|
||||||
|
"ip": "x.y.z.a",
|
||||||
|
"port": 3128,
|
||||||
|
"tg_url": "tg://proxy?...",
|
||||||
|
"tg_qrcode": "https://api.qrserver.com/v1/create-qr-code?data...",
|
||||||
|
"tme_url": "https://t.me/proxy?...",
|
||||||
|
"tme_qrcode": "https://api.qrserver.com/v1/create-qr-code?data..."
|
||||||
|
},
|
||||||
|
"secret": {
|
||||||
|
"hex": "...",
|
||||||
|
"base64": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or if you are using docker:
|
||||||
|
|
||||||
# statsd integration
|
```console
|
||||||
|
$ docker exec mtg-proxy /mtg access /config.toml
|
||||||
|
```
|
||||||
|
|
||||||
mtg provides an integration with statsd, you can enable it with command
|
## Doppelganger
|
||||||
line interface. To enable it, you have to provide IP address of statsd
|
|
||||||
service.
|
|
||||||
|
|
||||||
Out of the box, mtg supports 2 additional dialects: [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/)
|
mtg can mimic real websites, please take a look at relevant section in example
|
||||||
and [Datadog](https://docs.datadoghq.com/developers/dogstatsd/).
|
config file.
|
||||||
|
|
||||||
All metrics are gauges. Here is the list of metrics and their meaning:
|
mtg comes with some very good precollected statistics coming from
|
||||||
|
[ok.ru](https://ok.ru/). It does not mean that you have to cover yourself
|
||||||
|
by pretending that mtg is _ok.ru_. **Do not do that: ok.ru comes from very specific
|
||||||
|
ASNs, but not from VPS providers you are going to use.** What I want to say
|
||||||
|
is that defaults are very good enough to use as is because ok.ru for public
|
||||||
|
pages has a very generic profile of TLS packets delay.
|
||||||
|
|
||||||
| Metric name | Unit | Description |
|
But for better results it is recommended to teach mtg about the website you
|
||||||
|------------------------|---------|--------------------------------------------|
|
will use as a domain front. In order to do that, you need to specify URLs
|
||||||
| `connections` | number | The number of active connections. |
|
from this website. Just go to it, open WebDeveloper console and pick up
|
||||||
| `telegram_connections` | number | The number of active telegram connections. |
|
random URLs. For better results they have to be **from the same domain name
|
||||||
| `crashes` | number | An amount of crashes in client handlers. |
|
you are going to use as a disguise** but serve light and heavy content: pages,
|
||||||
| `traffic.egress` | bytes | Traffic from the start of application. |
|
images etc. Do not use many, 2-3 will probably work.
|
||||||
| `replay_attacks` | number | The number of prevented replay attacks. |
|
|
||||||
|
|
||||||
All metrics are prefixed with given prefix. Default prefix is `mtg`.
|
mtg will crawl these pages periodically, accumulating statistics and
|
||||||
Also, metrics provide tags (ipv4/ipv6, dc indexes etc).
|
using it as you go.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[defense.doppelganger]
|
||||||
|
urls = [
|
||||||
|
"https://lalala.com/index.html",
|
||||||
|
"https://lalala.com/contacts.html",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
# Prometheus integration
|
This is not very necessary. Keep in mind these rules:
|
||||||
|
|
||||||
[Prometheus](https://prometheus.io) integration comes out of
|
1. If you are not sure what is this all about, do nothing. Defaults are good.
|
||||||
the box, you do not need to setup anything special.
|
2. All URLs must be HTTPS
|
||||||
|
3. All URLs should be from the same domain name (but this is not a rule)
|
||||||
|
4. Do not use a lot of pages. Use _different_ pages. mtg will start using this
|
||||||
|
statistics when it will accumulate enough anyway.
|
||||||
|
5. These URLs should be directly accessible from mtg without proxies whatsoever
|
||||||
|
6. Do not create huge raids. mtg will repeatedly crawl in raids, making N repeats.
|
||||||
|
Do not use high N, you do not want to be noticeable.
|
||||||
|
7. It makes no sense to have small delay between raids. Usually webservers
|
||||||
|
do not update their TLS settings each hour.
|
||||||
|
8. If you have some specific knowledge if webserver is using
|
||||||
|
[TLS Dynamic Record Sizing](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/), you
|
||||||
|
can use a very specific setting. This are Cloudflare, Go standard webservers,
|
||||||
|
[caddy](https://caddyserver.com/) and [H2O](https://h2o.examp1e.net/). If so,
|
||||||
|
you can enable `drs` setting.
|
||||||
|
9. **If you are not sure, touch nothing!**
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
# Upgrade to 1.0
|
### `ip was blacklisted` for clients on the same LAN
|
||||||
|
|
||||||
Version 1.0 breaks compatibility with previous versions so please read
|
If you run mtg at home and a client on the same LAN (for example, your
|
||||||
this chapter carefully:
|
phone on the home Wi-Fi) cannot connect, check the proxy logs for a
|
||||||
|
message like:
|
||||||
|
|
||||||
1. mtg now uses subcommands. Please use `mtg run` instead of just
|
```json
|
||||||
`mtg` to run a proxy.
|
{"level":"info","ip":"10.0.1.1","logger":"proxy","message":"ip was blacklisted"}
|
||||||
2. Options which set host and port separately were removed in a
|
```
|
||||||
favor of fused `host:port` options.
|
|
||||||
3. Own stats server was removed. Prometheus endpoint is moved to
|
The reason is that the default blocklist (`firehol_level1.netset`)
|
||||||
default stats endpoint.
|
includes bogon networks, which covers all RFC1918 ranges
|
||||||
4. It is possible to connect to this proxy only with a secret which
|
(`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). Any client
|
||||||
was used to run it. So, no backward compatibility of clients.
|
connecting from such an address is rejected by the blocklist —
|
||||||
5. Multiplexing involves connectivity with middle proxies and involves
|
the TCP connection is closed immediately with no response, so
|
||||||
the most complex code path of this proxy. To avoid potential bugs,
|
from the client's point of view nothing loads at all.
|
||||||
we still recommend using direct mode.
|
|
||||||
|
There are three ways to resolve it:
|
||||||
|
|
||||||
|
1. Disable the blocklist entirely in `config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[defense.blocklist]
|
||||||
|
enabled = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Simplest option if the proxy is used only by you and people you trust.
|
||||||
|
|
||||||
|
2. Keep the blocklist but swap `firehol_level1` for a narrower list that
|
||||||
|
does not include bogons, for example `firehol_abusers_1d`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[defense.blocklist]
|
||||||
|
enabled = true
|
||||||
|
urls = ["https://iplists.firehol.org/files/firehol_abusers_1d.netset"]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Connect to the proxy through a public IP or domain name with hairpin
|
||||||
|
NAT (`MASQUERADE`) on your router. mtg will then see the client with
|
||||||
|
its public address and the blocklist will not match. This is more
|
||||||
|
work to set up but preserves full blocklist protection.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Out of the box, mtg works with
|
||||||
|
[statsd](https://github.com/statsd/statsd) and
|
||||||
|
[Prometheus](https://prometheus.io/). Please check configuration file
|
||||||
|
example to get how to set this integration up.
|
||||||
|
|
||||||
|
Here goes a list of metrics with their types but without a prefix.
|
||||||
|
|
||||||
|
| Name | Type | Tags | Description |
|
||||||
|
|-----------------------------|---------|----------------------------------|--------------------------------------------------------------------------------------------|
|
||||||
|
| client_connections | gauge | `ip_family` | Count of processing client connections. |
|
||||||
|
| telegram_connections | gauge | `telegram_ip`, `dc` | Count of connections to Telegram servers. |
|
||||||
|
| domain_fronting_connections | gauge | `ip_family` | Count of connections to fronting domain. |
|
||||||
|
| iplist_size | gauge | `ip_list` | A size of either allowlist or blocklist in use. |
|
||||||
|
| telegram_traffic | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram. |
|
||||||
|
| domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. |
|
||||||
|
| domain_fronting | counter | – | Count of domain fronting events. |
|
||||||
|
| concurrency_limited | counter | – | Count of events, when client connection was rejected due to concurrency limit. |
|
||||||
|
| ip_blocklisted | counter | `ip_list` | Count of events when client connection was rejected because IP was found in the blocklist. |
|
||||||
|
| replay_attacks | counter | – | Count of detected replay attacks. |
|
||||||
|
|
||||||
|
Tag meaning:
|
||||||
|
|
||||||
|
| Name | Values | Description |
|
||||||
|
|-------------|----------------------------|-----------------------------------------------|
|
||||||
|
| ip_family | `ipv4`, `ipv6` | A version of the IP protocol. |
|
||||||
|
| dc | | A number of the Telegram DC for a connection. |
|
||||||
|
| telegram_ip | | IP address of the Telegram server. |
|
||||||
|
| direction | `to_client`, `from_client` | A direction of the traffic flow. |
|
||||||
|
| ip_list | `allowlist`, `blocklist` | A type of the IP list. |
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We support 2 tracks: 2.x and 1.x
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 2.x | :white_check_mark: |
|
||||||
|
| 1.x | :interrobang: |
|
||||||
|
| < 1.0 | :x: |
|
||||||
|
|
||||||
|
1.x has adtag support. We do not plan any active development there but we guarantee:
|
||||||
|
|
||||||
|
1. Regular dependency updates
|
||||||
|
2. Updates for Golang versions
|
||||||
|
3. Security vulnerability fixes
|
||||||
|
4. Merging small PRs
|
||||||
|
|
||||||
|
2.x is in active development and have a full support.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you have found a vulnerability, please report about it to nineseconds@yandex.ru, telegram @9seconds or here in issues
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package antireplay
|
|
||||||
|
|
||||||
import "github.com/VictoriaMetrics/fastcache"
|
|
||||||
|
|
||||||
var (
|
|
||||||
prefixObfuscated2 = []byte{0x00}
|
|
||||||
prefixTLS = []byte{0x01}
|
|
||||||
)
|
|
||||||
|
|
||||||
type cache struct {
|
|
||||||
data *fastcache.Cache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) AddObfuscated2(data []byte) {
|
|
||||||
c.data.Set(keyObfuscated2(data), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) AddTLS(data []byte) {
|
|
||||||
c.data.Set(keyTLS(data), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) HasObfuscated2(data []byte) bool {
|
|
||||||
return c.data.Has(keyObfuscated2(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cache) HasTLS(data []byte) bool {
|
|
||||||
return c.data.Has(keyTLS(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyObfuscated2(data []byte) []byte {
|
|
||||||
return append(prefixObfuscated2, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyTLS(data []byte) []byte {
|
|
||||||
return append(prefixTLS, data...)
|
|
||||||
}
|
|
||||||
+13
-27
@@ -1,31 +1,17 @@
|
|||||||
|
// Antireplay package has cache implementations that are effective against
|
||||||
|
// replay attacks.
|
||||||
|
//
|
||||||
|
// To understand more about replay attacks, please read documentation for
|
||||||
|
// [mtglib.AntiReplayCache] interface. This package has a list of some
|
||||||
|
// implementations of this interface.
|
||||||
package antireplay
|
package antireplay
|
||||||
|
|
||||||
import (
|
const (
|
||||||
"sync"
|
// DefaultStableBloomFilterMaxSize is a recommended byte size for a stable
|
||||||
|
// bloom filter.
|
||||||
|
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
// DefaultStableBloomFilterErrorRate is a recommended default error rate for a
|
||||||
|
// stable bloom filter.
|
||||||
"github.com/9seconds/mtg/config"
|
DefaultStableBloomFilterErrorRate = 0.001
|
||||||
)
|
)
|
||||||
|
|
||||||
type CacheInterface interface {
|
|
||||||
AddObfuscated2([]byte)
|
|
||||||
AddTLS([]byte)
|
|
||||||
HasObfuscated2([]byte) bool
|
|
||||||
HasTLS([]byte) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
Cache CacheInterface
|
|
||||||
initOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
initOnce.Do(func() {
|
|
||||||
if config.C.AntiReplayMaxSize == 0 {
|
|
||||||
Cache = nilCache{}
|
|
||||||
} else {
|
|
||||||
Cache = cache{fastcache.New(config.C.AntiReplayMaxSize)}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package antireplay
|
|
||||||
|
|
||||||
type nilCache struct{}
|
|
||||||
|
|
||||||
func (n nilCache) AddObfuscated2(_ []byte) {}
|
|
||||||
func (n nilCache) AddTLS(_ []byte) {}
|
|
||||||
func (n nilCache) HasObfuscated2(_ []byte) bool { return false }
|
|
||||||
func (n nilCache) HasTLS(_ []byte) bool { return false }
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package antireplay
|
||||||
|
|
||||||
|
import "github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
|
||||||
|
type noop struct{}
|
||||||
|
|
||||||
|
func (n noop) SeenBefore(_ []byte) bool { return false }
|
||||||
|
|
||||||
|
// NewNoop returns an implementation that does nothing. A corresponding method
|
||||||
|
// always returns false, so this cache accepts everything you pass to it.
|
||||||
|
func NewNoop() mtglib.AntiReplayCache {
|
||||||
|
return noop{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package antireplay_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/antireplay"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoopTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoopTestSuite) TestOp() {
|
||||||
|
filter := antireplay.NewNoop()
|
||||||
|
|
||||||
|
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||||
|
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||||
|
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||||
|
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoop(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &NoopTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package antireplay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/OneOfOne/xxhash"
|
||||||
|
boom "github.com/tylertreat/BoomFilters"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stableBloomFilter struct {
|
||||||
|
filter boom.StableBloomFilter
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
return s.filter.TestAndAdd(digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStableBloomFilter returns an implementation of AntiReplayCache based on
|
||||||
|
// stable bloom filter.
|
||||||
|
//
|
||||||
|
// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
|
||||||
|
//
|
||||||
|
// The basic idea of a stable bloom filter is quite simple: each time when you
|
||||||
|
// set a new element, you randomly reset P elements. There is a hardcore math
|
||||||
|
// which proves that if you choose this P correctly, you can maintain the same
|
||||||
|
// error rate for a stream of elements.
|
||||||
|
//
|
||||||
|
// byteSize is the number of bytes you want to give to a bloom filter.
|
||||||
|
// errorRate is desired false-positive error rate. If you want to use default
|
||||||
|
// values, please pass 0 for byteSize and <0 for errorRate.
|
||||||
|
func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
|
||||||
|
if byteSize == 0 {
|
||||||
|
byteSize = DefaultStableBloomFilterMaxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorRate < 0 {
|
||||||
|
errorRate = DefaultStableBloomFilterErrorRate
|
||||||
|
}
|
||||||
|
|
||||||
|
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate)
|
||||||
|
sf.SetHash(xxhash.New64())
|
||||||
|
|
||||||
|
return &stableBloomFilter{
|
||||||
|
filter: *sf,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package antireplay_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/antireplay"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StableBloomFilterTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StableBloomFilterTestSuite) TestOp() {
|
||||||
|
filter := antireplay.NewStableBloomFilter(100000, 0.001)
|
||||||
|
|
||||||
|
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
|
||||||
|
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
|
||||||
|
suite.True(filter.SeenBefore([]byte{1, 2, 3}))
|
||||||
|
suite.True(filter.SeenBefore([]byte{4, 5, 6}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStableBloomFilter(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &StableBloomFilterTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "dev" // has to be set by ldflags
|
||||||
|
|
||||||
|
const (
|
||||||
|
buildInfoModuleStart byte = iota
|
||||||
|
buildInfoModuleFinish
|
||||||
|
buildInfoModuleDelimeter
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVersion() string {
|
||||||
|
buildInfo, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now()
|
||||||
|
commit := ""
|
||||||
|
goVersion := buildInfo.GoVersion
|
||||||
|
dirtySuffix := ""
|
||||||
|
|
||||||
|
for _, setting := range buildInfo.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.time":
|
||||||
|
date, _ = time.Parse(time.RFC3339, setting.Value)
|
||||||
|
case "vcs.revision":
|
||||||
|
commit = setting.Value
|
||||||
|
case "vcs.modified":
|
||||||
|
if dirty, _ := strconv.ParseBool(setting.Value); dirty {
|
||||||
|
dirtySuffix = " [dirty]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
|
||||||
|
checksumModule := func(mod *debug.Module) {
|
||||||
|
hasher.Write([]byte{buildInfoModuleStart})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Path) //nolint: errcheck
|
||||||
|
hasher.Write([]byte{buildInfoModuleDelimeter})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Version) //nolint: errcheck
|
||||||
|
hasher.Write([]byte{buildInfoModuleDelimeter})
|
||||||
|
|
||||||
|
io.WriteString(hasher, mod.Sum) //nolint: errcheck
|
||||||
|
|
||||||
|
hasher.Write([]byte{buildInfoModuleFinish})
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(hasher, buildInfo.Path) //nolint: errcheck
|
||||||
|
|
||||||
|
binary.Write(hasher, binary.LittleEndian, uint64(1+len(buildInfo.Deps))) //nolint: errcheck
|
||||||
|
|
||||||
|
sort.Slice(buildInfo.Deps, func(i, j int) bool {
|
||||||
|
return buildInfo.Deps[i].Path > buildInfo.Deps[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
checksumModule(&buildInfo.Main)
|
||||||
|
|
||||||
|
for _, module := range buildInfo.Deps {
|
||||||
|
checksumModule(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (%s: %s on %s%s, modules checksum %s)",
|
||||||
|
version,
|
||||||
|
goVersion,
|
||||||
|
date.Format(time.RFC3339),
|
||||||
|
commit,
|
||||||
|
dirtySuffix,
|
||||||
|
base64.StdEncoding.EncodeToString(hasher.Sum(nil)))
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Generate(secretType, hostname string) {
|
|
||||||
data := make([]byte, config.SimpleSecretLength)
|
|
||||||
if _, err := rand.Read(data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := hex.EncodeToString(data)
|
|
||||||
|
|
||||||
switch secretType {
|
|
||||||
case "simple":
|
|
||||||
PrintStdout(secret)
|
|
||||||
case "secured":
|
|
||||||
PrintStdout("dd" + secret)
|
|
||||||
default:
|
|
||||||
PrintStdout("ee" + secret + hex.EncodeToString([]byte(hostname)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/antireplay"
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
"github.com/9seconds/mtg/faketls"
|
|
||||||
"github.com/9seconds/mtg/hub"
|
|
||||||
"github.com/9seconds/mtg/ntp"
|
|
||||||
"github.com/9seconds/mtg/obfuscated2"
|
|
||||||
"github.com/9seconds/mtg/proxy"
|
|
||||||
"github.com/9seconds/mtg/stats"
|
|
||||||
"github.com/9seconds/mtg/telegram"
|
|
||||||
"github.com/9seconds/mtg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Proxy() error { // nolint: funlen
|
|
||||||
ctx := utils.GetSignalContext()
|
|
||||||
|
|
||||||
atom := zap.NewAtomicLevel()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case config.C.Debug:
|
|
||||||
atom.SetLevel(zapcore.DebugLevel)
|
|
||||||
case config.C.Verbose:
|
|
||||||
atom.SetLevel(zapcore.InfoLevel)
|
|
||||||
default:
|
|
||||||
atom.SetLevel(zapcore.ErrorLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoderCfg := zap.NewProductionEncoderConfig()
|
|
||||||
logger := zap.New(zapcore.NewCore(
|
|
||||||
zapcore.NewJSONEncoder(encoderCfg),
|
|
||||||
zapcore.Lock(os.Stderr),
|
|
||||||
atom,
|
|
||||||
))
|
|
||||||
|
|
||||||
zap.ReplaceGlobals(logger)
|
|
||||||
defer logger.Sync() // nolint: errcheck
|
|
||||||
|
|
||||||
if err := config.InitPublicAddress(ctx); err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.S().Debugw("Configuration", "config", config.Printable())
|
|
||||||
|
|
||||||
if len(config.C.AdTag) > 0 {
|
|
||||||
zap.S().Infow("Use middle proxy connection to Telegram")
|
|
||||||
|
|
||||||
diff, err := ntp.Fetch()
|
|
||||||
if err != nil {
|
|
||||||
Fatal("Cannot fetch time data from NTP")
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff > time.Second {
|
|
||||||
Fatal("Your local time is skewed and drift is bigger than a second. Please sync your time.")
|
|
||||||
}
|
|
||||||
|
|
||||||
go ntp.AutoUpdate()
|
|
||||||
} else {
|
|
||||||
zap.S().Infow("Use direct connection to Telegram")
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintJSONStdout(config.GetURLs())
|
|
||||||
|
|
||||||
if err := stats.Init(ctx); err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
antireplay.Init()
|
|
||||||
telegram.Init()
|
|
||||||
hub.Init(ctx)
|
|
||||||
|
|
||||||
proxyListener, err := net.Listen("tcp", config.C.Bind.String())
|
|
||||||
if err != nil {
|
|
||||||
Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
proxyListener.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
app := &proxy.Proxy{
|
|
||||||
Logger: zap.S().Named("proxy"),
|
|
||||||
Context: ctx,
|
|
||||||
ClientProtocolMaker: obfuscated2.MakeClientProtocol,
|
|
||||||
}
|
|
||||||
if config.C.SecretMode == config.SecretModeTLS {
|
|
||||||
app.ClientProtocolMaker = faketls.MakeClientProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Serve(proxyListener)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Fatal(arg interface{}) {
|
|
||||||
if value, ok := arg.(error); ok {
|
|
||||||
arg = fmt.Errorf("fatal error: %+v", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintStderr(arg)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintStderr(args ...interface{}) {
|
|
||||||
fmt.Fprintln(os.Stderr, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintStdout(args ...interface{}) {
|
|
||||||
fmt.Println(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintJSONStderr(data interface{}) {
|
|
||||||
printJSON(os.Stderr, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintJSONStdout(data interface{}) {
|
|
||||||
printJSON(os.Stdout, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printJSON(writer io.Writer, data interface{}) {
|
|
||||||
encoder := json.NewEncoder(writer)
|
|
||||||
encoder.SetEscapeHTML(false)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/alecthomas/units"
|
|
||||||
statsd "github.com/smira/go-statsd"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SecretMode uint8
|
|
||||||
|
|
||||||
func (s SecretMode) String() string {
|
|
||||||
switch s {
|
|
||||||
case SecretModeSimple:
|
|
||||||
return "simple"
|
|
||||||
case SecretModeSecured:
|
|
||||||
return "secured"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "tls"
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
SecretModeSimple SecretMode = iota
|
|
||||||
SecretModeSecured
|
|
||||||
SecretModeTLS
|
|
||||||
)
|
|
||||||
|
|
||||||
type PreferIP uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
PreferIPv4 PreferIP = iota
|
|
||||||
PreferIPv6
|
|
||||||
)
|
|
||||||
|
|
||||||
const SimpleSecretLength = 16
|
|
||||||
|
|
||||||
type OptionType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
OptionTypeDebug OptionType = iota
|
|
||||||
OptionTypeVerbose
|
|
||||||
|
|
||||||
OptionTypePreferIP
|
|
||||||
|
|
||||||
OptionTypeBind
|
|
||||||
OptionTypePublicIPv4
|
|
||||||
OptionTypePublicIPv6
|
|
||||||
|
|
||||||
OptionTypeStatsBind
|
|
||||||
OptionTypeStatsNamespace
|
|
||||||
OptionTypeStatsdAddress
|
|
||||||
OptionTypeStatsdTagsFormat
|
|
||||||
OptionTypeStatsdTags
|
|
||||||
|
|
||||||
OptionTypeWriteBufferSize
|
|
||||||
OptionTypeReadBufferSize
|
|
||||||
|
|
||||||
OptionTypeCloakPort
|
|
||||||
|
|
||||||
OptionTypeAntiReplayMaxSize
|
|
||||||
|
|
||||||
OptionTypeMultiplexPerConnection
|
|
||||||
|
|
||||||
OptionTypeNTPServers
|
|
||||||
|
|
||||||
OptionTypeSecret
|
|
||||||
OptionTypeAdtag
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Bind *net.TCPAddr `json:"bind"`
|
|
||||||
PublicIPv4 *net.TCPAddr `json:"public_ipv4"`
|
|
||||||
PublicIPv6 *net.TCPAddr `json:"public_ipv6"`
|
|
||||||
StatsBind *net.TCPAddr `json:"stats_bind"`
|
|
||||||
StatsdAddr *net.TCPAddr `json:"stats_addr"`
|
|
||||||
StatsdTagsFormat *statsd.TagFormat `json:"statsd_tags_format"`
|
|
||||||
|
|
||||||
StatsNamespace string `json:"stats_namespace"`
|
|
||||||
CloakHost string `json:"cloak_host"`
|
|
||||||
StatsdTags map[string]string `json:"statsd_tags"`
|
|
||||||
|
|
||||||
WriteBuffer int `json:"write_buffer"`
|
|
||||||
ReadBuffer int `json:"read_buffer"`
|
|
||||||
CloakPort int `json:"cloak_port"`
|
|
||||||
|
|
||||||
AntiReplayMaxSize int `json:"anti_replay_max_size"`
|
|
||||||
|
|
||||||
MultiplexPerConnection int `json:"multiplex_per_connection"`
|
|
||||||
|
|
||||||
Debug bool `json:"debug"`
|
|
||||||
Verbose bool `json:"verbose"`
|
|
||||||
SecretMode SecretMode `json:"secret_mode"`
|
|
||||||
PreferIP PreferIP `json:"prefer_ip"`
|
|
||||||
NTPServers []string `json:"ntp_servers"`
|
|
||||||
|
|
||||||
Secret []byte `json:"secret"`
|
|
||||||
AdTag []byte `json:"adtag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Opt struct {
|
|
||||||
Option OptionType
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var C = Config{}
|
|
||||||
|
|
||||||
func Init(options ...Opt) error { // nolint: gocyclo, funlen
|
|
||||||
for _, opt := range options {
|
|
||||||
switch opt.Option {
|
|
||||||
case OptionTypeDebug:
|
|
||||||
C.Debug = opt.Value.(bool)
|
|
||||||
case OptionTypeVerbose:
|
|
||||||
C.Verbose = opt.Value.(bool)
|
|
||||||
case OptionTypePreferIP:
|
|
||||||
value := opt.Value.(string)
|
|
||||||
switch value {
|
|
||||||
case "ipv4":
|
|
||||||
C.PreferIP = PreferIPv4
|
|
||||||
case "ipv6":
|
|
||||||
C.PreferIP = PreferIPv6
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("incorrect direct IP mode %s", value)
|
|
||||||
}
|
|
||||||
case OptionTypeBind:
|
|
||||||
C.Bind = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypePublicIPv4:
|
|
||||||
C.PublicIPv4 = opt.Value.(*net.TCPAddr)
|
|
||||||
if C.PublicIPv4 == nil {
|
|
||||||
C.PublicIPv4 = &net.TCPAddr{}
|
|
||||||
}
|
|
||||||
case OptionTypePublicIPv6:
|
|
||||||
C.PublicIPv6 = opt.Value.(*net.TCPAddr)
|
|
||||||
if C.PublicIPv6 == nil {
|
|
||||||
C.PublicIPv6 = &net.TCPAddr{}
|
|
||||||
}
|
|
||||||
case OptionTypeStatsBind:
|
|
||||||
C.StatsBind = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypeStatsNamespace:
|
|
||||||
C.StatsNamespace = opt.Value.(string)
|
|
||||||
case OptionTypeStatsdAddress:
|
|
||||||
C.StatsdAddr = opt.Value.(*net.TCPAddr)
|
|
||||||
case OptionTypeStatsdTagsFormat:
|
|
||||||
value := opt.Value.(string)
|
|
||||||
switch value {
|
|
||||||
case "datadog":
|
|
||||||
C.StatsdTagsFormat = statsd.TagFormatDatadog
|
|
||||||
case "influxdb":
|
|
||||||
C.StatsdTagsFormat = statsd.TagFormatInfluxDB
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("incorrect statsd tag %s", value)
|
|
||||||
}
|
|
||||||
case OptionTypeStatsdTags:
|
|
||||||
C.StatsdTags = opt.Value.(map[string]string)
|
|
||||||
case OptionTypeWriteBufferSize:
|
|
||||||
C.WriteBuffer = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeReadBufferSize:
|
|
||||||
C.ReadBuffer = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeCloakPort:
|
|
||||||
C.CloakPort = int(opt.Value.(uint16))
|
|
||||||
case OptionTypeAntiReplayMaxSize:
|
|
||||||
C.AntiReplayMaxSize = int(opt.Value.(units.Base2Bytes))
|
|
||||||
case OptionTypeMultiplexPerConnection:
|
|
||||||
C.MultiplexPerConnection = int(opt.Value.(uint))
|
|
||||||
case OptionTypeNTPServers:
|
|
||||||
C.NTPServers = opt.Value.([]string)
|
|
||||||
if len(C.NTPServers) == 0 {
|
|
||||||
return errors.New("ntp server list is empty")
|
|
||||||
}
|
|
||||||
case OptionTypeSecret:
|
|
||||||
C.Secret = opt.Value.([]byte)
|
|
||||||
case OptionTypeAdtag:
|
|
||||||
C.AdTag = opt.Value.([]byte)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown tag %v", opt.Option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(C.Secret) == 1+SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xdd}):
|
|
||||||
C.SecretMode = SecretModeSecured
|
|
||||||
C.Secret = bytes.TrimPrefix(C.Secret, []byte{0xdd})
|
|
||||||
case len(C.Secret) > SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xee}):
|
|
||||||
C.SecretMode = SecretModeTLS
|
|
||||||
secret := bytes.TrimPrefix(C.Secret, []byte{0xee})
|
|
||||||
C.Secret = secret[:SimpleSecretLength]
|
|
||||||
C.CloakHost = string(secret[SimpleSecretLength:])
|
|
||||||
case len(C.Secret) == SimpleSecretLength:
|
|
||||||
C.SecretMode = SecretModeSimple
|
|
||||||
default:
|
|
||||||
return errors.New("incorrect secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.MultiplexPerConnection == 0 {
|
|
||||||
return errors.New("cannot use 0 clients per connection for multiplexing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.CloakHost != "" {
|
|
||||||
if _, err := net.LookupHost(C.CloakHost); err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve address of host", "hostname", C.CloakHost, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitPublicAddress(ctx context.Context) error {
|
|
||||||
if C.PublicIPv4.Port == 0 {
|
|
||||||
C.PublicIPv4.Port = C.Bind.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.PublicIPv6.Port == 0 {
|
|
||||||
C.PublicIPv6.Port = C.Bind.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
foundAddress := C.PublicIPv4.IP != nil || C.PublicIPv6.IP != nil
|
|
||||||
|
|
||||||
if C.PublicIPv4.IP == nil {
|
|
||||||
ip, err := getGlobalIPv4(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
|
||||||
} else {
|
|
||||||
C.PublicIPv4.IP = ip
|
|
||||||
foundAddress = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.PublicIPv6.IP == nil {
|
|
||||||
ip, err := getGlobalIPv6(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Warnw("Cannot resolve public address", "error", err)
|
|
||||||
} else {
|
|
||||||
C.PublicIPv6.IP = ip
|
|
||||||
foundAddress = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundAddress {
|
|
||||||
return errors.New("cannot resolve any public address")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Printable() interface{} {
|
|
||||||
data, err := json.Marshal(C)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rv := map[string]interface{}{}
|
|
||||||
if err := json.Unmarshal(data, &rv); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ifconfigAddress = "https://ifconfig.co/ip"
|
|
||||||
ifconfigTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func getGlobalIPv4(ctx context.Context) (net.IP, error) {
|
|
||||||
ip, err := fetchIP(ctx, "tcp4")
|
|
||||||
if err != nil || ip.To4() == nil {
|
|
||||||
return nil, fmt.Errorf("cannot find public ipv4 address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGlobalIPv6(ctx context.Context) (net.IP, error) {
|
|
||||||
ip, err := fetchIP(ctx, "tcp6")
|
|
||||||
if err != nil || ip.To4() != nil {
|
|
||||||
return nil, fmt.Errorf("cannot find public ipv6 address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIP(ctx context.Context, network string) (net.IP, error) {
|
|
||||||
dialer := &net.Dialer{FallbackDelay: -1}
|
|
||||||
client := &http.Client{
|
|
||||||
Jar: nil,
|
|
||||||
Timeout: ifconfigTimeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, addr)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", ifconfigAddress, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create a request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
if resp != nil {
|
|
||||||
io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("cannot perform a request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
|
|
||||||
respDataBytes, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
respData := strings.TrimSpace(string(respDataBytes))
|
|
||||||
|
|
||||||
ip := net.ParseIP(respData)
|
|
||||||
if ip == nil {
|
|
||||||
return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type URLs struct {
|
|
||||||
TG string `json:"tg_url"`
|
|
||||||
TMe string `json:"tme_url"`
|
|
||||||
TGQRCode string `json:"tg_qrcode"`
|
|
||||||
TMeQRCode string `json:"tme_qrcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPURLs struct {
|
|
||||||
IPv4 URLs `json:"ipv4"`
|
|
||||||
IPv6 URLs `json:"ipv6"`
|
|
||||||
BotSecret string `json:"secret_for_mtproxybot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetURLs() (urls IPURLs) {
|
|
||||||
secret := ""
|
|
||||||
|
|
||||||
switch C.SecretMode {
|
|
||||||
case SecretModeSimple:
|
|
||||||
secret = hex.EncodeToString(C.Secret)
|
|
||||||
case SecretModeSecured:
|
|
||||||
secret = "dd" + hex.EncodeToString(C.Secret)
|
|
||||||
case SecretModeTLS:
|
|
||||||
secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost))
|
|
||||||
}
|
|
||||||
|
|
||||||
urls.IPv4 = makeURLs(C.PublicIPv4, secret)
|
|
||||||
urls.IPv6 = makeURLs(C.PublicIPv6, secret)
|
|
||||||
urls.BotSecret = hex.EncodeToString(C.Secret)
|
|
||||||
|
|
||||||
return urls
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeURLs(addr *net.TCPAddr, secret string) (urls URLs) {
|
|
||||||
values := url.Values{}
|
|
||||||
values.Set("server", addr.IP.String())
|
|
||||||
values.Set("port", strconv.Itoa(addr.Port))
|
|
||||||
values.Set("secret", secret)
|
|
||||||
|
|
||||||
urls.TG = makeTGURL(values)
|
|
||||||
urls.TMe = makeTMeURL(values)
|
|
||||||
urls.TGQRCode = makeQRCodeURL(urls.TG)
|
|
||||||
urls.TMeQRCode = makeQRCodeURL(urls.TG)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTGURL(values url.Values) string {
|
|
||||||
tgURL := url.URL{
|
|
||||||
Scheme: "tg",
|
|
||||||
Host: "proxy",
|
|
||||||
RawQuery: values.Encode(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return tgURL.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTMeURL(values url.Values) string {
|
|
||||||
tMeURL := url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "t.me",
|
|
||||||
Path: "proxy",
|
|
||||||
RawQuery: values.Encode(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return tMeURL.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeQRCodeURL(data string) string {
|
|
||||||
qr := url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "api.qrserver.com",
|
|
||||||
Path: "v1/create-qr-code",
|
|
||||||
}
|
|
||||||
|
|
||||||
values := url.Values{}
|
|
||||||
values.Set("qzone", "4")
|
|
||||||
values.Set("format", "svg")
|
|
||||||
values.Set("data", data)
|
|
||||||
qr.RawQuery = values.Encode()
|
|
||||||
|
|
||||||
return qr.String()
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionAcks struct {
|
|
||||||
Simple bool
|
|
||||||
Quick bool
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type DC int16
|
|
||||||
|
|
||||||
const DCDefaultIdx DC = 1
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ConnIDLength = 8
|
|
||||||
|
|
||||||
type ConnID [ConnIDLength]byte
|
|
||||||
|
|
||||||
func (c ConnID) String() string {
|
|
||||||
return hex.EncodeToString(c[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnID() ConnID {
|
|
||||||
var id ConnID
|
|
||||||
|
|
||||||
if _, err := rand.Read(id[:]); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type Packet []byte
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionProtocol uint8
|
|
||||||
|
|
||||||
func (c ConnectionProtocol) String() string {
|
|
||||||
switch c {
|
|
||||||
case ConnectionProtocolAny:
|
|
||||||
return "any"
|
|
||||||
case ConnectionProtocolIPv4:
|
|
||||||
return "ipv4"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConnectionProtocolIPv4 ConnectionProtocol = 1
|
|
||||||
ConnectionProtocolIPv6 = ConnectionProtocolIPv4 << 1
|
|
||||||
ConnectionProtocolAny = ConnectionProtocolIPv4 | ConnectionProtocolIPv6
|
|
||||||
)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
type ConnectionType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConnectionTypeUnknown ConnectionType = iota
|
|
||||||
ConnectionTypeAbridged
|
|
||||||
ConnectionTypeIntermediate
|
|
||||||
ConnectionTypeSecure
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef}
|
|
||||||
ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee}
|
|
||||||
ConnectionTagSecure = []byte{0xdd, 0xdd, 0xdd, 0xdd}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t ConnectionType) Tag() []byte {
|
|
||||||
switch t {
|
|
||||||
case ConnectionTypeAbridged:
|
|
||||||
return ConnectionTagAbridged
|
|
||||||
case ConnectionTypeIntermediate:
|
|
||||||
return ConnectionTagIntermediate
|
|
||||||
default:
|
|
||||||
return ConnectionTagSecure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Wrap interface {
|
|
||||||
Conn() net.Conn
|
|
||||||
Logger() *zap.SugaredLogger
|
|
||||||
LocalAddr() *net.TCPAddr
|
|
||||||
RemoteAddr() *net.TCPAddr
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type PacketAckReader interface {
|
|
||||||
Read(*ConnectionAcks) (Packet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckWriter interface {
|
|
||||||
Write(Packet, *ConnectionAcks) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckCloser interface {
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadCloser interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckWriteCloser interface {
|
|
||||||
PacketAckWriter
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadWriter interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckReadWriteCloser interface {
|
|
||||||
PacketAckReader
|
|
||||||
PacketAckWriter
|
|
||||||
PacketAckCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketAckFullReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
PacketAckReadWriteCloser
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type BasePacketReader interface {
|
|
||||||
Read() (Packet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BasePacketWriter interface {
|
|
||||||
Write(Packet) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReader interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketReader
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketWriter interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketReader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadWriter interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
BasePacketReader
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
BasePacketWriter
|
|
||||||
BasePacketReader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package conntypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaseStreamReaderWithTimeout interface {
|
|
||||||
ReadTimeout([]byte, time.Duration) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseStreamWriterWithTimeout interface {
|
|
||||||
WriteTimeout([]byte, time.Duration) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReader interface {
|
|
||||||
Wrap
|
|
||||||
io.Reader
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamWriter interface {
|
|
||||||
Wrap
|
|
||||||
io.Writer
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadCloser
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.WriteCloser
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadWriter interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadWriter
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamReadWriteCloser interface {
|
|
||||||
Wrap
|
|
||||||
io.ReadWriteCloser
|
|
||||||
BaseStreamReaderWithTimeout
|
|
||||||
BaseStreamWriterWithTimeout
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
# Caddy sits behind HAProxy which passes raw TLS through on :8443.
|
||||||
|
# ACME HTTP-01 challenges arrive on :80 via HAProxy's acl passthrough.
|
||||||
|
http_port 80
|
||||||
|
https_port 8443
|
||||||
|
|
||||||
|
# HAProxy forwards connections to :8443 with a PROXY protocol v2
|
||||||
|
# header (see haproxy.cfg `send-proxy-v2`). The proxy_protocol
|
||||||
|
# listener wrapper strips the header and exposes the real client IP
|
||||||
|
# to Caddy's access log. The `tls` wrapper must follow so that TLS
|
||||||
|
# is terminated on the unwrapped connection.
|
||||||
|
#
|
||||||
|
# `allow` lists the networks permitted to send PROXY headers. These
|
||||||
|
# ranges cover docker compose's default bridge networks; tighten
|
||||||
|
# them if you pin a specific subnet in docker-compose.yml.
|
||||||
|
servers :8443 {
|
||||||
|
listener_wrappers {
|
||||||
|
proxy_protocol {
|
||||||
|
timeout 5s
|
||||||
|
allow 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||||
|
}
|
||||||
|
tls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{$DOMAIN} {
|
||||||
|
root * /srv
|
||||||
|
file_server
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# SNI-routing deployment for mtg
|
||||||
|
|
||||||
|
A turnkey `docker compose` setup that puts an SNI-aware TCP router
|
||||||
|
(HAProxy) in front of mtg **and** a real web server (Caddy with
|
||||||
|
automatic HTTPS).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Modern DPI systems actively probe suspected proxies. If the server
|
||||||
|
closes the connection or returns something unexpected, the IP gets
|
||||||
|
flagged. With this setup:
|
||||||
|
|
||||||
|
- **Telegram clients** connect to port 443, HAProxy sees the configured
|
||||||
|
SNI and routes them to mtg (FakeTLS).
|
||||||
|
- **Everything else** (browsers, DPI probes, scanners) is routed to
|
||||||
|
Caddy, which responds with a real Let's Encrypt certificate and serves
|
||||||
|
genuine web content.
|
||||||
|
|
||||||
|
Because your domain's DNS points to this server, the SNI/IP match is
|
||||||
|
natural and passive DPI has nothing to flag.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Point your domain's DNS A/AAAA record to this server's IP.
|
||||||
|
|
||||||
|
# 2. Generate an mtg secret:
|
||||||
|
docker run --rm nineseconds/mtg:2 generate-secret --hex YOUR_DOMAIN
|
||||||
|
|
||||||
|
# 3. Edit the config files:
|
||||||
|
# - mtg-config.toml → paste the secret
|
||||||
|
# - haproxy.cfg → replace "example.com" in the SNI ACL
|
||||||
|
# - .env or export → DOMAIN=your.domain
|
||||||
|
|
||||||
|
# 4. (Optional) put your site content into www/
|
||||||
|
|
||||||
|
# 5. Start:
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 6. Verify:
|
||||||
|
# - Open https://YOUR_DOMAIN in a browser → you should see the web page
|
||||||
|
# - Configure Telegram with the proxy link from:
|
||||||
|
docker compose exec mtg mtg access /config/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real client IPs (PROXY protocol)
|
||||||
|
|
||||||
|
HAProxy forwards TCP connections to mtg and Caddy with a PROXY protocol
|
||||||
|
v2 header so both backends see the real client IP instead of HAProxy's
|
||||||
|
container address. The three pieces must stay in sync:
|
||||||
|
|
||||||
|
- `haproxy.cfg` — `send-proxy-v2` on the `mtg` and `web` backend `server` lines
|
||||||
|
- `mtg-config.toml` — `proxy-protocol-listener = true`
|
||||||
|
- `Caddyfile` — `listener_wrappers { proxy_protocol { ... } tls }` on `:8443`
|
||||||
|
|
||||||
|
If you disable one, disable all three, otherwise the backend will fail
|
||||||
|
to parse the connection.
|
||||||
|
|
||||||
|
## ACME (Let's Encrypt) notes
|
||||||
|
|
||||||
|
HAProxy passes `/.well-known/acme-challenge/` requests on `:80` to
|
||||||
|
Caddy so that HTTP-01 validation works out of the box. Make sure your
|
||||||
|
domain's DNS A/AAAA record points to this server before starting.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
:443 ──────>│ HAProxy │
|
||||||
|
│ (TCP, SNI peek) │
|
||||||
|
└──┬───────────┬───┘
|
||||||
|
SNI match │ │ default
|
||||||
|
v v
|
||||||
|
┌─────────┐ ┌─────────┐
|
||||||
|
│ mtg │ │ Caddy │
|
||||||
|
│ :3128 │ │ :8443 │
|
||||||
|
│ FakeTLS │ │ real TLS│
|
||||||
|
└─────────┘ └─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `docker-compose.yml` | Service definitions |
|
||||||
|
| `haproxy.cfg` | SNI routing rules — **edit the domain** |
|
||||||
|
| `mtg-config.toml` | mtg proxy config — **paste your secret** |
|
||||||
|
| `Caddyfile` | Web server config (auto-HTTPS) |
|
||||||
|
| `www/` | Static site content served by Caddy |
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# SNI-routing deployment: HAProxy (443) -> mtg + real web backend
|
||||||
|
#
|
||||||
|
# This setup puts an SNI-aware TCP router in front of mtg so that:
|
||||||
|
# - Telegram clients (FakeTLS with the correct SNI) are routed to mtg
|
||||||
|
# - All other TLS traffic (including DPI probes) reaches the real web
|
||||||
|
# server, which responds with a genuine certificate
|
||||||
|
#
|
||||||
|
# The result: active probes see a real website; passive DPI sees matching
|
||||||
|
# SNI/IP because the domain resolves to this server's IP.
|
||||||
|
#
|
||||||
|
# Quick start:
|
||||||
|
# 1. Set YOUR_DOMAIN below (and in mtg-config.toml)
|
||||||
|
# 2. docker compose up -d
|
||||||
|
# 3. mtg generate-secret YOUR_DOMAIN -> put it in mtg-config.toml
|
||||||
|
# 4. docker compose restart mtg
|
||||||
|
#
|
||||||
|
# See BEST_PRACTICES.md and the project wiki for background.
|
||||||
|
|
||||||
|
services:
|
||||||
|
haproxy:
|
||||||
|
image: haproxy:lts-alpine
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||||
|
depends_on:
|
||||||
|
- mtg
|
||||||
|
- web
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
mtg:
|
||||||
|
image: nineseconds/mtg:2
|
||||||
|
volumes:
|
||||||
|
- ./mtg-config.toml:/config/config.toml:ro
|
||||||
|
expose:
|
||||||
|
- "3128"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: caddy:alpine
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- ./www:/srv:ro
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
- "8443"
|
||||||
|
environment:
|
||||||
|
DOMAIN: ${DOMAIN:-example.com}
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# HAProxy SNI router — Layer 4 (TCP mode)
|
||||||
|
#
|
||||||
|
# Inspects the SNI in the TLS ClientHello and routes traffic:
|
||||||
|
# - SNI matching the mtg secret domain -> mtg (FakeTLS / MTProto)
|
||||||
|
# - Everything else -> real web backend (Caddy)
|
||||||
|
#
|
||||||
|
# Because routing happens before TLS termination, each backend sees the
|
||||||
|
# raw ClientHello and handles TLS itself. The real web backend therefore
|
||||||
|
# presents a genuine certificate to any probe or browser.
|
||||||
|
|
||||||
|
global
|
||||||
|
log stdout format raw local0 info
|
||||||
|
maxconn 4096
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
mode tcp
|
||||||
|
option tcplog
|
||||||
|
timeout connect 5s
|
||||||
|
timeout client 60s
|
||||||
|
timeout server 60s
|
||||||
|
|
||||||
|
# --- HTTP :80 — ACME challenges + redirect -----------------------------------
|
||||||
|
|
||||||
|
frontend http
|
||||||
|
bind *:80
|
||||||
|
mode http
|
||||||
|
|
||||||
|
# Let Caddy answer ACME HTTP-01 challenges for Let's Encrypt.
|
||||||
|
acl is_acme path_beg /.well-known/acme-challenge/
|
||||||
|
use_backend web_acme if is_acme
|
||||||
|
|
||||||
|
http-request redirect scheme https code 301
|
||||||
|
|
||||||
|
# --- TLS :443 — SNI-based routing -------------------------------------------
|
||||||
|
|
||||||
|
frontend tls
|
||||||
|
bind *:443
|
||||||
|
tcp-request inspect-delay 5s
|
||||||
|
tcp-request content accept if { req_ssl_hello_type 1 }
|
||||||
|
|
||||||
|
# Route Telegram clients to mtg.
|
||||||
|
# Replace "example.com" with the domain from your mtg secret.
|
||||||
|
use_backend mtg if { req_ssl_sni -i example.com }
|
||||||
|
|
||||||
|
default_backend web
|
||||||
|
|
||||||
|
backend mtg
|
||||||
|
# send-proxy-v2 prepends a PROXY protocol v2 header so mtg sees the
|
||||||
|
# real client IP instead of HAProxy's. mtg must have
|
||||||
|
# `proxy-protocol-listener = true` in its config.
|
||||||
|
server mtg mtg:3128 send-proxy-v2
|
||||||
|
|
||||||
|
backend web
|
||||||
|
# send-proxy-v2 prepends a PROXY protocol v2 header so Caddy logs the
|
||||||
|
# real client IP instead of HAProxy's. Caddy must enable the
|
||||||
|
# proxy_protocol listener wrapper on :8443 (see Caddyfile).
|
||||||
|
server web web:8443 send-proxy-v2
|
||||||
|
|
||||||
|
backend web_acme
|
||||||
|
mode http
|
||||||
|
server web web:80
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Minimal mtg configuration for the SNI-router setup.
|
||||||
|
#
|
||||||
|
# 1. Generate a secret: mtg generate-secret --hex example.com
|
||||||
|
# 2. Paste it below.
|
||||||
|
# 3. Replace example.com with your actual domain everywhere.
|
||||||
|
|
||||||
|
secret = "PASTE_YOUR_SECRET_HERE"
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
|
|
||||||
|
# HAProxy in front sends PROXY protocol v2 headers so mtg can see the
|
||||||
|
# real client IP. Keep this in sync with haproxy.cfg (`send-proxy-v2`).
|
||||||
|
proxy-protocol-listener = true
|
||||||
|
|
||||||
|
[defense.anti-replay]
|
||||||
|
enabled = true
|
||||||
|
max-size = "1mib"
|
||||||
|
error-rate = 0.001
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="utf-8"><title>Welcome</title></head>
|
||||||
|
<body><h1>It works!</h1><p>Replace this with your own content.</p></body>
|
||||||
|
</html>
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
package essentials
|
||||||
|
|
||||||
|
// TelegramCoreAddresses are publicly known addresses of Telegram core network.
|
||||||
|
var TelegramCoreAddresses = map[int][]string{
|
||||||
|
1: {
|
||||||
|
"149.154.175.50:443",
|
||||||
|
"[2001:b28:f23d:f001::a]:443",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"149.154.167.51:443",
|
||||||
|
"95.161.76.100:443",
|
||||||
|
"[2001:67c:04e8:f002::a]:443",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"149.154.175.100:443",
|
||||||
|
"[2001:b28:f23d:f003::a]:443",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
"149.154.167.91:443",
|
||||||
|
"[2001:67c:04e8:f004::a]:443",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
"149.154.171.5:443",
|
||||||
|
"[2001:b28:f23f:f005::a]:443",
|
||||||
|
},
|
||||||
|
203: {
|
||||||
|
"91.105.192.100:443",
|
||||||
|
"[2a0a:f280:0203:a:5000::100]:443",
|
||||||
|
},
|
||||||
|
10001: {
|
||||||
|
"149.154.175.10:443",
|
||||||
|
"[2001:b28:f23d:f001::e]:443",
|
||||||
|
},
|
||||||
|
10002: {
|
||||||
|
"149.154.167.40:443",
|
||||||
|
"[2001:67c:4e8:f002::e]:443",
|
||||||
|
},
|
||||||
|
10003: {
|
||||||
|
"149.154.175.117:443",
|
||||||
|
"[2001:b28:f23d:f003::e]:443",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package essentials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloseableReader is an [io.Reader] interface that can close its reading end.
|
||||||
|
type CloseableReader interface {
|
||||||
|
io.Reader
|
||||||
|
CloseRead() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseableWriter is an [io.Writer] that can close its writing end.
|
||||||
|
type CloseableWriter interface {
|
||||||
|
io.Writer
|
||||||
|
CloseWrite() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn is an extension of [net.Conn] that can close its ends. This mostly
|
||||||
|
// implies TCP connections.
|
||||||
|
type Conn interface {
|
||||||
|
net.Conn
|
||||||
|
CloseableReader
|
||||||
|
CloseableWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
type netConnWrapper struct {
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n netConnWrapper) CloseRead() error {
|
||||||
|
if conn, ok := n.Conn.(CloseableReader); ok {
|
||||||
|
return conn.CloseRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n netConnWrapper) CloseWrite() error {
|
||||||
|
if conn, ok := n.Conn.(CloseableWriter); ok {
|
||||||
|
return conn.CloseWrite()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapConn wraps a generic [net.Conn] into Conn.
|
||||||
|
func WrapNetConn(conn net.Conn) Conn {
|
||||||
|
return netConnWrapper{conn}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// This is a minimal package that contains _essentials_ of mtglib and its
|
||||||
|
// complimentary packages. This is mostly required to comply some interfaces
|
||||||
|
// between mtglib and its internals to avoid circular dependencies.
|
||||||
|
//
|
||||||
|
// This package should contain only bare minimum and mostly technical.
|
||||||
|
package essentials
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand/v2"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/OneOfOne/xxhash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventStream is a default implementation of the [mtglib.EventStream]
|
||||||
|
// interface.
|
||||||
|
//
|
||||||
|
// EventStream manages a set of goroutines, observers. Main
|
||||||
|
// responsibility of the event stream is to route an event to relevant
|
||||||
|
// observer based on some hash so each observer will have all events
|
||||||
|
// which belong to some stream id.
|
||||||
|
//
|
||||||
|
// Thus, EventStream can spawn many observers.
|
||||||
|
type EventStream struct {
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
chans []chan mtglib.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send starts delivering of the message to observer with respect to a
|
||||||
|
// given context If context is closed, message could be not delivered.
|
||||||
|
func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
|
||||||
|
var chanNo uint32
|
||||||
|
|
||||||
|
if streamID := evt.StreamID(); streamID != "" {
|
||||||
|
chanNo = xxhash.ChecksumString32(streamID)
|
||||||
|
} else {
|
||||||
|
chanNo = rand.Uint32()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
case e.chans[chanNo%uint32(len(e.chans))] <- evt:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops an event stream pipeline.
|
||||||
|
func (e EventStream) Shutdown() {
|
||||||
|
e.ctxCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventStream builds a new default event stream.
|
||||||
|
//
|
||||||
|
// If you give an empty array of observers, then NoopObserver is going
|
||||||
|
// to be used. If you give many observers, then they will process a
|
||||||
|
// message concurrently.
|
||||||
|
func NewEventStream(observerFactories []ObserverFactory) EventStream {
|
||||||
|
if len(observerFactories) == 0 {
|
||||||
|
observerFactories = append(observerFactories, NewNoopObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
rv := EventStream{
|
||||||
|
ctx: ctx,
|
||||||
|
ctxCancel: cancel,
|
||||||
|
chans: make([]chan mtglib.Event, runtime.NumCPU()),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range runtime.NumCPU() {
|
||||||
|
rv.chans[i] = make(chan mtglib.Event, 1)
|
||||||
|
|
||||||
|
if len(observerFactories) == 1 {
|
||||||
|
go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]())
|
||||||
|
} else {
|
||||||
|
go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { //nolint: cyclop
|
||||||
|
defer observer.Shutdown()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case evt := <-eventChan:
|
||||||
|
switch typedEvt := evt.(type) {
|
||||||
|
case mtglib.EventTraffic:
|
||||||
|
observer.EventTraffic(typedEvt)
|
||||||
|
case mtglib.EventStart:
|
||||||
|
observer.EventStart(typedEvt)
|
||||||
|
case mtglib.EventFinish:
|
||||||
|
observer.EventFinish(typedEvt)
|
||||||
|
case mtglib.EventConnectedToDC:
|
||||||
|
observer.EventConnectedToDC(typedEvt)
|
||||||
|
case mtglib.EventDomainFronting:
|
||||||
|
observer.EventDomainFronting(typedEvt)
|
||||||
|
case mtglib.EventIPBlocklisted:
|
||||||
|
observer.EventIPBlocklisted(typedEvt)
|
||||||
|
case mtglib.EventConcurrencyLimited:
|
||||||
|
observer.EventConcurrencyLimited(typedEvt)
|
||||||
|
case mtglib.EventReplayAttack:
|
||||||
|
observer.EventReplayAttack(typedEvt)
|
||||||
|
case mtglib.EventIPListSize:
|
||||||
|
observer.EventIPListSize(typedEvt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package events_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/events"
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventStreamTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
observerMock1 *ObserverMock
|
||||||
|
observerMock2 *ObserverMock
|
||||||
|
stream events.EventStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) SetupTest() {
|
||||||
|
suite.ctx, suite.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
suite.observerMock1 = &ObserverMock{}
|
||||||
|
suite.observerMock2 = &ObserverMock{}
|
||||||
|
|
||||||
|
suite.observerMock1.On("Shutdown")
|
||||||
|
suite.observerMock2.On("Shutdown")
|
||||||
|
|
||||||
|
factories := make([]events.ObserverFactory, 2)
|
||||||
|
factories[0] = func() events.Observer { return suite.observerMock1 }
|
||||||
|
factories[1] = func() events.Observer { return suite.observerMock2 }
|
||||||
|
|
||||||
|
suite.stream = events.NewEventStream(factories)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventStart() {
|
||||||
|
evt := mtglib.NewEventStart("connID", net.ParseIP("10.0.0.1"))
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventStart", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventStart)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventConnectedToDC() {
|
||||||
|
evt := mtglib.NewEventConnectedToDC("connID", net.ParseIP("10.0.0.1"), 3)
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventConnectedToDC", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventConnectedToDC)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.DC, caught.DC)
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventDomainFronting() {
|
||||||
|
evt := mtglib.NewEventDomainFronting("connID")
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventDomainFronting", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventDomainFronting)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventTraffic() {
|
||||||
|
evt := mtglib.NewEventTraffic("connID", 1024, true)
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventTraffic", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventTraffic)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
suite.Equal(evt.Traffic, caught.Traffic)
|
||||||
|
suite.Equal(evt.IsRead, caught.IsRead)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventFinish() {
|
||||||
|
evt := mtglib.NewEventFinish("connID")
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventFinish", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventFinish)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventConcurrencyLimited() {
|
||||||
|
evt := mtglib.NewEventConcurrencyLimited()
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventConcurrencyLimited", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventConcurrencyLimited)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
suite.Empty(evt.StreamID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventIPBlocklisted() {
|
||||||
|
evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventIPBlocklisted", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventIPBlocklisted)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventReplayAttack() {
|
||||||
|
evt := mtglib.NewEventReplayAttack("CONNID")
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventReplayAttack", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventReplayAttack)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.StreamID(), caught.StreamID())
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TestEventIPListSize() {
|
||||||
|
evt := mtglib.NewEventIPListSize(10, true)
|
||||||
|
|
||||||
|
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
|
||||||
|
v.
|
||||||
|
On("EventIPListSize", mock.Anything).
|
||||||
|
Once().
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
caught, ok := args.Get(0).(mtglib.EventIPListSize)
|
||||||
|
|
||||||
|
suite.True(ok)
|
||||||
|
suite.Equal(evt.Timestamp(), caught.Timestamp())
|
||||||
|
suite.Equal(evt.Size, caught.Size)
|
||||||
|
suite.Equal(evt.IsBlockList, caught.IsBlockList)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.stream.Send(suite.ctx, evt)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EventStreamTestSuite) TearDownTest() {
|
||||||
|
suite.stream.Shutdown()
|
||||||
|
suite.ctxCancel()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
suite.observerMock1.AssertExpectations(suite.T())
|
||||||
|
suite.observerMock2.AssertExpectations(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventStream(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &EventStreamTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Events has a default implementations of EventStream for mtglib.
|
||||||
|
//
|
||||||
|
// Please see documentation for [mtglib.EventStream] interface to get an idea
|
||||||
|
// of such an abstraction. This package has implementations for the default
|
||||||
|
// event stream.
|
||||||
|
//
|
||||||
|
// Default event stream has a list of its own concepts. First, all it does is a
|
||||||
|
// routing of messages to known observers. It takes an event, defines its type
|
||||||
|
// and pass this message to a method of the observer.
|
||||||
|
//
|
||||||
|
// There might be many observers, but default event stream has a guarantee
|
||||||
|
// though. It uses StreamID as a sharding key and guarantees that a message
|
||||||
|
// with the same StreamID will be devlivered to the same observer instance. So,
|
||||||
|
// each producer is guarateed to get all relevant messages related to the same
|
||||||
|
// session. It is not possible that it will get EventFinish if it has not seen
|
||||||
|
// EventStart for that session yet.
|
||||||
|
package events
|
||||||
|
|
||||||
|
import "github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
|
||||||
|
// Observer is an instance that listens for the incoming events.
|
||||||
|
//
|
||||||
|
// As it is said in the package description, the default event stream
|
||||||
|
// guarantees that all events with the same StreamID are going to be routed to
|
||||||
|
// the same instance of the observer. So, there is no need to synchronize
|
||||||
|
// information about streams between many observers instances, they can have
|
||||||
|
// their local storage.
|
||||||
|
type Observer interface {
|
||||||
|
// EventStart reacts on incoming mtglib.EventStart event.
|
||||||
|
EventStart(mtglib.EventStart)
|
||||||
|
|
||||||
|
// EventFinish reacts on incoming mtglib.EventFinish event.
|
||||||
|
EventFinish(mtglib.EventFinish)
|
||||||
|
|
||||||
|
// EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC
|
||||||
|
// event.
|
||||||
|
EventConnectedToDC(mtglib.EventConnectedToDC)
|
||||||
|
|
||||||
|
// EventDomainFronting reacts on incoming mtglib.EventDomainFronting
|
||||||
|
// event.
|
||||||
|
EventDomainFronting(mtglib.EventDomainFronting)
|
||||||
|
|
||||||
|
// EventTraffic reacts on incoming mtglib.EventTraffic event.
|
||||||
|
EventTraffic(mtglib.EventTraffic)
|
||||||
|
|
||||||
|
// EventConcurrencyLimited reacts on incoming
|
||||||
|
// mtglib.EventConcurrencyLimited event.
|
||||||
|
EventConcurrencyLimited(mtglib.EventConcurrencyLimited)
|
||||||
|
|
||||||
|
// EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event.
|
||||||
|
EventIPBlocklisted(mtglib.EventIPBlocklisted)
|
||||||
|
|
||||||
|
// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
|
||||||
|
EventReplayAttack(mtglib.EventReplayAttack)
|
||||||
|
|
||||||
|
// EventIPListSize reacts on incoming mtglib.EventIPListSize
|
||||||
|
EventIPListSize(mtglib.EventIPListSize)
|
||||||
|
|
||||||
|
// Shutdown stop observer. Default event stream guarantees:
|
||||||
|
// 1. If shutdown is executed, it is executed only once
|
||||||
|
// 2. Observer won't receieve any new message after this
|
||||||
|
// function call.
|
||||||
|
Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObserverFactory creates a new instance of the observer.
|
||||||
|
//
|
||||||
|
// Default event stream creates a small set of goroutines to manage incoming
|
||||||
|
// messages. Each message is routed to an appropriate observer based on a
|
||||||
|
// sharding key, stream id. So, it is possible that an instance of mtg will
|
||||||
|
// have many observer instances, not a single one.
|
||||||
|
type ObserverFactory func() Observer
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package events_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObserverMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventStart(evt mtglib.EventStart) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) EventIPListSize(evt mtglib.EventIPListSize) {
|
||||||
|
o.Called(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObserverMock) Shutdown() {
|
||||||
|
o.Called()
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiObserver struct {
|
||||||
|
observers []Observer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventStart(evt mtglib.EventStart) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventStart(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventConnectedToDC(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventDomainFronting(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventTraffic(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventFinish(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventConcurrencyLimited(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventIPBlocklisted(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventReplayAttack(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) EventIPListSize(evt mtglib.EventIPListSize) {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
for _, v := range m.observers {
|
||||||
|
wg.Go(func() {
|
||||||
|
v.EventIPListSize(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiObserver) Shutdown() {
|
||||||
|
for _, v := range m.observers {
|
||||||
|
v.Shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMultiObserver(factories []ObserverFactory) Observer {
|
||||||
|
observers := make([]Observer, len(factories))
|
||||||
|
|
||||||
|
for i, v := range factories {
|
||||||
|
observers[i] = v()
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiObserver{
|
||||||
|
observers: observers,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noop struct{}
|
||||||
|
|
||||||
|
func (n noop) Send(ctx context.Context, evt mtglib.Event) {}
|
||||||
|
|
||||||
|
// NewNoopStream creates a stream which discards each message.
|
||||||
|
func NewNoopStream() mtglib.EventStream {
|
||||||
|
return noop{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopObserver struct{}
|
||||||
|
|
||||||
|
func (n noopObserver) EventStart(_ mtglib.EventStart) {}
|
||||||
|
func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC) {}
|
||||||
|
func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting) {}
|
||||||
|
func (n noopObserver) EventTraffic(_ mtglib.EventTraffic) {}
|
||||||
|
func (n noopObserver) EventFinish(_ mtglib.EventFinish) {}
|
||||||
|
func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
|
||||||
|
func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {}
|
||||||
|
func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {}
|
||||||
|
func (n noopObserver) EventIPListSize(_ mtglib.EventIPListSize) {}
|
||||||
|
func (n noopObserver) Shutdown() {}
|
||||||
|
|
||||||
|
// NewNoopObserver creates an observer which discards each message.
|
||||||
|
func NewNoopObserver() Observer {
|
||||||
|
return noopObserver{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package events_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/events"
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoopTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
testData map[string]mtglib.Event
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoopTestSuite) SetupSuite() {
|
||||||
|
suite.testData = map[string]mtglib.Event{
|
||||||
|
"start": mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")),
|
||||||
|
"connected-to-dc": mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2),
|
||||||
|
"domain-fronting": mtglib.NewEventDomainFronting("connID"),
|
||||||
|
"traffic": mtglib.NewEventTraffic("connID", 1000, true),
|
||||||
|
"finish": mtglib.NewEventFinish("connID"),
|
||||||
|
"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
|
||||||
|
"ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
|
||||||
|
"replay-attack": mtglib.NewEventReplayAttack("connID"),
|
||||||
|
"ip-list-size": mtglib.NewEventIPListSize(10, true),
|
||||||
|
}
|
||||||
|
suite.ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoopTestSuite) TestStream() {
|
||||||
|
stream := events.NewNoopStream()
|
||||||
|
|
||||||
|
for name, v := range suite.testData {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
suite.T().Run(name, func(t *testing.T) {
|
||||||
|
stream.Send(suite.ctx, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NoopTestSuite) TestObserver() {
|
||||||
|
observer := events.NewNoopObserver()
|
||||||
|
|
||||||
|
for name, v := range suite.testData {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
suite.T().Run(name, func(t *testing.T) {
|
||||||
|
switch typedEvt := value.(type) {
|
||||||
|
case mtglib.EventStart:
|
||||||
|
observer.EventStart(typedEvt)
|
||||||
|
case mtglib.EventConnectedToDC:
|
||||||
|
observer.EventConnectedToDC(typedEvt)
|
||||||
|
case mtglib.EventDomainFronting:
|
||||||
|
observer.EventDomainFronting(typedEvt)
|
||||||
|
case mtglib.EventFinish:
|
||||||
|
observer.EventFinish(typedEvt)
|
||||||
|
case mtglib.EventConcurrencyLimited:
|
||||||
|
observer.EventConcurrencyLimited(typedEvt)
|
||||||
|
case mtglib.EventIPBlocklisted:
|
||||||
|
observer.EventIPBlocklisted(typedEvt)
|
||||||
|
case mtglib.EventReplayAttack:
|
||||||
|
observer.EventReplayAttack(typedEvt)
|
||||||
|
case mtglib.EventIPListSize:
|
||||||
|
observer.EventIPListSize(typedEvt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoop(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &NoopTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
# This is an example of the configuration file for mtg. You actually can
|
||||||
|
# run mtg with it. It starts a proxy on all interfaces with a secret
|
||||||
|
# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
|
||||||
|
#
|
||||||
|
# It has all possible options with default values. So, a real world
|
||||||
|
# configuration file should contain only those options you are going to
|
||||||
|
# use. You do not need to enumerate all of them. In other words, each
|
||||||
|
# option here has a default value. If you comment a key-value pair, it
|
||||||
|
# should not make any effect.
|
||||||
|
#
|
||||||
|
# stats is the only exception.
|
||||||
|
|
||||||
|
# Debug starts application in debug mode. It starts to be quite verbose
|
||||||
|
# in output. Actually, the idea is that you run it in debug mode only if
|
||||||
|
# you have any issue.
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# A secret. Please remember that mtg supports only FakeTLS mode, legacy
|
||||||
|
# simple and secured mode are prohibited. For you it means that secret
|
||||||
|
# should either be base64-encoded or starts with ee.
|
||||||
|
secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
|
||||||
|
|
||||||
|
# Host:port pair to run proxy on.
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
|
|
||||||
|
# This defines what types of traffic mtg listens to. If you are not sure,
|
||||||
|
# then definitely keep it disable. Enable it only and only if incoming traffic
|
||||||
|
# is coming from some sort of load-balancer like HAProxy or ELB.
|
||||||
|
# https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt
|
||||||
|
#
|
||||||
|
# mtg uses a library that supports v1 and v2 versions of ProxyProtocol.
|
||||||
|
# default value is false.
|
||||||
|
# proxy-protocol-listener = false
|
||||||
|
|
||||||
|
# Defines how many concurrent connections are allowed to this proxy.
|
||||||
|
# All other incoming connections are going to be dropped.
|
||||||
|
concurrency = 8192
|
||||||
|
|
||||||
|
# Sometimes you want to enforce mtg to use some types of
|
||||||
|
# IP connectivity to Telegram. We have 4 modes:
|
||||||
|
# - prefer-ipv6:
|
||||||
|
# We can use both ipv4 and ipv6 but ipv6 has a preference
|
||||||
|
# - prefer-ipv4:
|
||||||
|
# We can use both ipv4 and ipv6 but ipv4 has a preference
|
||||||
|
# - only-ipv6:
|
||||||
|
# Only ipv6 connectivity is used
|
||||||
|
# - only-ipv4:
|
||||||
|
# Only ipv4 connectivity is used
|
||||||
|
prefer-ip = "prefer-ipv6"
|
||||||
|
|
||||||
|
# Public IP addresses of this server. Used by 'mtg access' to generate
|
||||||
|
# proxy links and by 'mtg doctor' to validate SNI-DNS match.
|
||||||
|
# If not set, mtg tries to detect them automatically via ifconfig.co.
|
||||||
|
# Set these if ifconfig.co is unreachable from your server.
|
||||||
|
# public-ipv4 = "1.2.3.4"
|
||||||
|
# public-ipv6 = "2001:db8::1"
|
||||||
|
|
||||||
|
# If this setting is set, then mtg will try to get proxy updates from Telegram
|
||||||
|
# Usually this is completely fine to have it disabled, because mtg has a list
|
||||||
|
# of some core proxies hardcoded.
|
||||||
|
auto-update = false
|
||||||
|
|
||||||
|
# FakeTLS uses domain fronting protection. So it needs to know a port to
|
||||||
|
# access.
|
||||||
|
#
|
||||||
|
# Deprecated: use [domain-fronting] configuration block. If relevant option
|
||||||
|
# is defined there, this one would be ignored.
|
||||||
|
# domain-fronting-port = 443
|
||||||
|
|
||||||
|
# By default, mtg resolves the fronting hostname (from the secret) via DNS
|
||||||
|
# to establish a TCP connection. If DNS resolution of that hostname is blocked,
|
||||||
|
# you can specify an IP address to connect to directly. The hostname is still
|
||||||
|
# used for SNI in the TLS handshake.
|
||||||
|
#
|
||||||
|
# default value is not set (DNS resolution is used).
|
||||||
|
#
|
||||||
|
# Deprecated: use [domain-fronting] configuration block. If relevant option
|
||||||
|
# is defined there, this one would be ignored.
|
||||||
|
# domain-fronting-ip = "10.0.0.10"
|
||||||
|
|
||||||
|
# This makes a communication between both fronting website and mtg to use
|
||||||
|
# proxy protocol.
|
||||||
|
#
|
||||||
|
# Deprecated: use [domain-fronting] configuration block. If relevant option
|
||||||
|
# is defined there, this one would be ignored.
|
||||||
|
# domain-fronting-proxy-protocol = false
|
||||||
|
|
||||||
|
# FakeTLS can compare timestamps to prevent probes. Each message has
|
||||||
|
# encrypted timestamp. So, mtg can compare this timestamp and decide if
|
||||||
|
# we need to proceed with connection or not.
|
||||||
|
#
|
||||||
|
# Sometimes time can be skewed so we accept all messages within a
|
||||||
|
# time range of this parameter.
|
||||||
|
tolerate-time-skewness = "5s"
|
||||||
|
|
||||||
|
# Telegram has a concept of DC. You can think about DC as a number of a cluster
|
||||||
|
# with a certain purpose. Some clusters serve media, some - messages, some rule
|
||||||
|
# channels and so on. But sometimes unknown DC number is requested by client.
|
||||||
|
# It could be a bug or some global reconfiguration of the Telegram.
|
||||||
|
#
|
||||||
|
# By default, proxy rejects such requests. But it is also possible to fallback
|
||||||
|
# this request to any DC. Telegram works in a way that any DC is able to serve
|
||||||
|
# any request but sacrificing a latency.
|
||||||
|
#
|
||||||
|
# If this setting is disabled (default), mtg will reject a connection.
|
||||||
|
# Otherwise, chose a new DC.
|
||||||
|
allow-fallback-on-unknown-dc = false
|
||||||
|
|
||||||
|
# This section is relevant to communication with fronting domain. Usually
|
||||||
|
# you do not need to setup anything here but there are plenty of cases, especially
|
||||||
|
# if you put mtg behind load balancer, when some specific configuration is
|
||||||
|
# required.
|
||||||
|
[domain-fronting]
|
||||||
|
# By default, mtg resolves the fronting hostname (from the secret) via DNS
|
||||||
|
# to establish a TCP connection. If DNS resolution of that hostname is blocked,
|
||||||
|
# you can specify an IP address to connect to directly. The hostname is still
|
||||||
|
# used for SNI in the TLS handshake.
|
||||||
|
#
|
||||||
|
# default value is not set (DNS resolution is used).
|
||||||
|
# ip = "10.10.10.11"
|
||||||
|
|
||||||
|
# FakeTLS uses domain fronting protection. So it needs to know a port to
|
||||||
|
# access. Default value is 443
|
||||||
|
# port = 443
|
||||||
|
|
||||||
|
# This makes a communication between both fronting website and mtg to use
|
||||||
|
# proxy protocol.
|
||||||
|
# proxy-protocol = false
|
||||||
|
|
||||||
|
# network defines different network-related settings
|
||||||
|
[network]
|
||||||
|
# please be aware that mtg needs to do some external requests. For
|
||||||
|
# example, if you do not pass public ips, it will request your public ip
|
||||||
|
# address from some external service.
|
||||||
|
#
|
||||||
|
# As for 2.0, if you set a public-ip on your own, mtg won't issue any
|
||||||
|
# network requests except of those required for Telegram.
|
||||||
|
#
|
||||||
|
# so, in order of doing them, it needs to do DNS lookup. mtg ignores DNS
|
||||||
|
# resolver of the operating system and uses DOH instead. This is a host
|
||||||
|
# it has to access.
|
||||||
|
#
|
||||||
|
# By default we use Cloudflare.
|
||||||
|
#
|
||||||
|
# DEPRECATED option:
|
||||||
|
# If dns option is specified, it will be used instead. No default is defined
|
||||||
|
# anymore. But if you set this option, this is literally the same providing
|
||||||
|
# https:// URL to `dns`.
|
||||||
|
#
|
||||||
|
# In other words:
|
||||||
|
#
|
||||||
|
# doh-ip = "1.1.1.1"
|
||||||
|
#
|
||||||
|
# is literally the same as
|
||||||
|
#
|
||||||
|
# dns = "https://1.1.1.1"
|
||||||
|
# doh-ip = "1.1.1.1"
|
||||||
|
|
||||||
|
# Starting from mtg v2.1.12 we have changed a configuration for DNS. Now it
|
||||||
|
# supports DNS-over-HTTPS, DNS-over-TLS, custom UDP resolver and system
|
||||||
|
# resolver.
|
||||||
|
#
|
||||||
|
# Here is how to define DNS-over-HTTPS:
|
||||||
|
# - https://1.1.1.1
|
||||||
|
# - https://1.1.1.1/dns-query
|
||||||
|
# - https://cloudflare-dns.com/dns-query
|
||||||
|
# - https://cloudflare-dns.com
|
||||||
|
#
|
||||||
|
# Here is how to define DNS-over-TLS:
|
||||||
|
# - tls://1.1.1.1
|
||||||
|
# - tls://cloudflare-dns.com
|
||||||
|
#
|
||||||
|
# Here is how to define a custom UDP resolver (we support only IPs here)
|
||||||
|
# - 1.1.1.1
|
||||||
|
# - udp://1.1.1.1
|
||||||
|
#
|
||||||
|
# If you set it to empty string, default resolver will be used.
|
||||||
|
# But please comment out doh-ip
|
||||||
|
dns = "https://1.1.1.1"
|
||||||
|
|
||||||
|
# mtg can work via proxies (for now, we support only socks5). Proxy
|
||||||
|
# configuration is done via list. So, you can specify many proxies
|
||||||
|
# there.
|
||||||
|
#
|
||||||
|
# Actually, if you supply an empty list, then no proxies are going to be
|
||||||
|
# used. If you supply a single proxy, then mtg will use it exclusively.
|
||||||
|
# If you supply >= 2, then mtg will load balance between them.
|
||||||
|
#
|
||||||
|
# If you add an empty string here, this is an equivalent of 'plain network',
|
||||||
|
# with no proxy usage.
|
||||||
|
#
|
||||||
|
# Proxy configuration is done via ordinary URI schema:
|
||||||
|
#
|
||||||
|
# socks5://user:password@host:port
|
||||||
|
#
|
||||||
|
# Only socks5 proxy is used. user/password is optional. As you can
|
||||||
|
# see, you can specify some parameters in GET query. These parameters
|
||||||
|
# configure circuit breaker.
|
||||||
|
proxies = [
|
||||||
|
# "socks5://user:password@host:port"
|
||||||
|
]
|
||||||
|
|
||||||
|
# network timeouts define different settings for timeouts. tcp timeout
|
||||||
|
# define a global timeout on establishing of network connections. idle
|
||||||
|
# means a timeout on pumping data between sockset when nothing is
|
||||||
|
# happening.
|
||||||
|
[network.timeout]
|
||||||
|
tcp = "5s"
|
||||||
|
http = "10s"
|
||||||
|
idle = "5m"
|
||||||
|
handshake = "10s"
|
||||||
|
|
||||||
|
# this defines a configuration for TCP keep alives. Default values are taken
|
||||||
|
# from Golang default behavior.
|
||||||
|
[network.keep-alive]
|
||||||
|
disabled = false
|
||||||
|
# idle means a time period after which we start sending TCP Keep Alive probes
|
||||||
|
idle = "15s"
|
||||||
|
# interval is a period between 2 consecutive probes
|
||||||
|
interval = "15s"
|
||||||
|
# if we miss that many probes, a connection will be considered as a dead one.
|
||||||
|
count = 9
|
||||||
|
|
||||||
|
# mtg has to mimic real websites. It does not mean domain fronting, it also
|
||||||
|
# means that traffic characteristics should be similar to real world traffic.
|
||||||
|
# websites and applications behave differently, their traffic patterns are also
|
||||||
|
# different. Applications do bursts of RPC-style messages (or JSON communication,
|
||||||
|
# does not really matter), while websites pump heavy content in HTTP2 streams
|
||||||
|
#
|
||||||
|
# It means that statistically there is a different between traffic shape:
|
||||||
|
# delays between packets are also different.
|
||||||
|
# In order to avoid censorship detection based on these patterns, there is a
|
||||||
|
# mtg subsystem called "Doppelganger" that aims to mimic website statistics
|
||||||
|
# as close as it could.
|
||||||
|
#
|
||||||
|
# Delays between TLS packets are not constant. There are many factors
|
||||||
|
# that come in play. Application should generate some response, it could
|
||||||
|
# send some headers first and stream content with chunked encoding. So
|
||||||
|
# some first packets could come as soon as possible, with some delays
|
||||||
|
# after first ones. Such phenomenon is described by different statistic
|
||||||
|
# distribution. There are 2 distribution that describe it: lognormal
|
||||||
|
# distribution and Weibul distribution. Lognormal is all about steady streams
|
||||||
|
# of heavy content like a video. Weibul is great about short bursts like
|
||||||
|
# user who requested a static page an a couple of images.
|
||||||
|
[defense.doppelganger]
|
||||||
|
# This is a list of URLs that would be crawled by mtg to approximate delay
|
||||||
|
# statistics. They MUST be HTTPS urls.
|
||||||
|
#
|
||||||
|
# You can come to the website and collect different URLs, with light and
|
||||||
|
# heavy content. We recommend to search for CDNs.
|
||||||
|
urls = [
|
||||||
|
# "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
|
||||||
|
]
|
||||||
|
# A collection is done in raids. Each raid makes this number of requests to
|
||||||
|
# each URL in this list. Do not use a huge number, 10 is probably ok.
|
||||||
|
repeats-per-raid = 10
|
||||||
|
# This is a duration between each raid. It makes no sense to have a small number
|
||||||
|
# here as you would start to make a noticeable activity. Usually traffic patterns
|
||||||
|
# do not change a lot, so do not expect different results if you request
|
||||||
|
# each 10 minutes.
|
||||||
|
raid-each = "6h"
|
||||||
|
# This enables dynamic tls record sizing.
|
||||||
|
#
|
||||||
|
# Some modern stacks and platforms start to use the technique that is called
|
||||||
|
# DRS. They start with small TLS packets and ramp up eventually. First packets
|
||||||
|
# are usually about MTU size, after that we get 4k and eventually max size.
|
||||||
|
# This is done with a good intention: to minimize a time to the first byte,
|
||||||
|
# so application could start doing something with the data right after first
|
||||||
|
# RTT.
|
||||||
|
#
|
||||||
|
# Apparently, about 90% of application do not employ this technique, they use
|
||||||
|
# max size always: nginx, apache, java stuff. But Golang tools, angie and
|
||||||
|
# some specific patches activate this technique.
|
||||||
|
#
|
||||||
|
# In order to mimic a real website we need to know something about software
|
||||||
|
# it uses. Usually nobody cares: openssl does 16384, Python does it, nginx
|
||||||
|
# does it. So this setting is disabled by default.
|
||||||
|
#
|
||||||
|
# https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
|
||||||
|
# https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
|
||||||
|
# https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
|
||||||
|
drs = false
|
||||||
|
|
||||||
|
# Some countries do active probing on Telegram connections. This technique
|
||||||
|
# allows to protect from such effort.
|
||||||
|
#
|
||||||
|
# mtg has a cache of some connection fingerprints. Actually, first bytes
|
||||||
|
# of each connection. So, it stores them in some in-memory LRU+TTL cache.
|
||||||
|
# You can configure this cache here.
|
||||||
|
[defense.anti-replay]
|
||||||
|
# You can enable/disable this feature.
|
||||||
|
enabled = true
|
||||||
|
# max size of such a cache. Please be aware that this number is
|
||||||
|
# approximate we try hard to store data quite dense but it is possible
|
||||||
|
# that we can go over this limit for 10-20% under some conditions and
|
||||||
|
# architectures.
|
||||||
|
max-size = "1mib"
|
||||||
|
# we use stable bloom filters for anti-replay cache. This helps
|
||||||
|
# to maintain a desired error ratio.
|
||||||
|
error-rate = 0.001
|
||||||
|
|
||||||
|
# You can protect proxies by using different blocklists. If client has
|
||||||
|
# ip from the given range, we do not try to do a proper handshake. We
|
||||||
|
# actually route it to fronting domain. So, this client will never ever
|
||||||
|
# have a chance to use mtg to access Telegram.
|
||||||
|
#
|
||||||
|
# Please remember that blocklists are initialized in async way. So,
|
||||||
|
# when you start a proxy, blocklists are empty, they are populated and
|
||||||
|
# processed in backgrounds. An error in any URL is ignored.
|
||||||
|
[defense.blocklist]
|
||||||
|
# You can enable/disable this feature.
|
||||||
|
enabled = true
|
||||||
|
# This is a limiter for concurrency. In order to protect website
|
||||||
|
# from overloading, we download files in this number of threads.
|
||||||
|
download-concurrency = 2
|
||||||
|
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
|
||||||
|
# You can provider links here (starts with https:// or http://) or
|
||||||
|
# path to a local file, but in this case it should be absolute.
|
||||||
|
#
|
||||||
|
# NOTE: the default list below (firehol_level1.netset) includes bogon
|
||||||
|
# networks, and therefore RFC1918 ranges as well (10.0.0.0/8,
|
||||||
|
# 172.16.0.0/12, 192.168.0.0/16). If you run mtg on a home/LAN network
|
||||||
|
# and connect from a client on the same LAN, that client will be
|
||||||
|
# rejected with "ip was blacklisted" and the connection dropped (TCP
|
||||||
|
# close, no response). If you see this, you can either disable this section
|
||||||
|
# (enabled = false), replace firehol_level1 with a narrower list that
|
||||||
|
# does not include bogons (e.g. firehol_abusers_1d), or connect via
|
||||||
|
# a public IP/domain with hairpin NAT on your router. See README for
|
||||||
|
# details.
|
||||||
|
urls = [
|
||||||
|
"https://iplists.firehol.org/files/firehol_level1.netset",
|
||||||
|
# "/local.file"
|
||||||
|
]
|
||||||
|
# How often do we need to update a blocklist set.
|
||||||
|
update-each = "24h"
|
||||||
|
|
||||||
|
# Allowlist is an opposite to a blocklist. Only those IPs that are coming from
|
||||||
|
# subnets defined in these lists are allowed. All others will be rejected.
|
||||||
|
#
|
||||||
|
# If this feature is disabled, then there won't be any check performed by this
|
||||||
|
# validator. It is possible to combine both blocklist and whitelist.
|
||||||
|
[defense.allowlist]
|
||||||
|
# You can enable/disable this feature.
|
||||||
|
enabled = false
|
||||||
|
# This is a limiter for concurrency. In order to protect website
|
||||||
|
# from overloading, we download files in this number of threads.
|
||||||
|
download-concurrency = 2
|
||||||
|
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
|
||||||
|
# You can provider links here (starts with https:// or http://) or
|
||||||
|
# path to a local file, but in this case it should be absolute.
|
||||||
|
urls = [
|
||||||
|
# "https://iplists.firehol.org/files/firehol_level1.netset",
|
||||||
|
# "/local.file"
|
||||||
|
|
||||||
|
]
|
||||||
|
update-each = "24h"
|
||||||
|
|
||||||
|
# statsd statistics integration.
|
||||||
|
[stats.statsd]
|
||||||
|
# enabled/disabled
|
||||||
|
enabled = false
|
||||||
|
# host:port for UDP endpoint of statsd
|
||||||
|
address = "127.0.0.1:8888"
|
||||||
|
# prefix of metric for statsd
|
||||||
|
metric-prefix = "mtg"
|
||||||
|
# tag format to use
|
||||||
|
# supported values are 'datadog', 'influxdb' and 'graphite'
|
||||||
|
# default format is graphite.
|
||||||
|
tag-format = "datadog"
|
||||||
|
|
||||||
|
# prometheus metrics integration.
|
||||||
|
[stats.prometheus]
|
||||||
|
# enabled/disabled
|
||||||
|
enabled = true
|
||||||
|
# host:port where to start http server for endpoint
|
||||||
|
bind-to = "127.0.0.1:3129"
|
||||||
|
# prefix of http path
|
||||||
|
http-path = "/"
|
||||||
|
# prefix for metrics for prometheus
|
||||||
|
metric-prefix = "mtg"
|
||||||
|
|
||||||
|
[dc]
|
||||||
|
dc1 = [
|
||||||
|
"149.154.175.50:443",
|
||||||
|
"149.154.175.54:443",
|
||||||
|
"149.154.175.59:443",
|
||||||
|
"[2001:b28:f23d:f001::a]:443"
|
||||||
|
]
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/antireplay"
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/obfuscated2"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
"github.com/9seconds/mtg/stats"
|
|
||||||
"github.com/9seconds/mtg/tlstypes"
|
|
||||||
"github.com/9seconds/mtg/wrappers/stream"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClientProtocol struct {
|
|
||||||
obfuscated2.ClientProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) {
|
|
||||||
rewinded := stream.NewRewind(socket)
|
|
||||||
bufferedReader := bufio.NewReader(rewinded)
|
|
||||||
|
|
||||||
for _, expected := range faketlsStartBytes {
|
|
||||||
if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected {
|
|
||||||
rewinded.Rewind()
|
|
||||||
c.cloakHost(rewinded)
|
|
||||||
|
|
||||||
return nil, errors.New("failed first bytes of tls handshake")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rewinded.Rewind()
|
|
||||||
rewinded = stream.NewRewind(rewinded)
|
|
||||||
|
|
||||||
if err := c.tlsHandshake(rewinded); err != nil {
|
|
||||||
rewinded.Rewind()
|
|
||||||
c.cloakHost(rewinded)
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed tls handshake: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := stream.NewFakeTLS(socket)
|
|
||||||
conn, err := c.ClientProtocol.Handshake(conn)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error {
|
|
||||||
helloRecord, err := tlstypes.ReadRecord(conn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot read initial record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := acquireBytesBuffer()
|
|
||||||
defer releaseBytesBuffer(buf)
|
|
||||||
|
|
||||||
helloRecord.Data.WriteBytes(buf)
|
|
||||||
|
|
||||||
clientHello, err := tlstypes.ParseClientHello(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse client hello: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := clientHello.Digest()
|
|
||||||
for i := 0; i < len(digest)-4; i++ {
|
|
||||||
if digest[i] != 0 {
|
|
||||||
return errBadDigest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:]))
|
|
||||||
createdAt := time.Unix(timestamp, 0)
|
|
||||||
timeDiff := time.Since(createdAt)
|
|
||||||
|
|
||||||
if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot {
|
|
||||||
return errBadTime
|
|
||||||
}
|
|
||||||
|
|
||||||
if antireplay.Cache.HasTLS(clientHello.Random[:]) {
|
|
||||||
stats.Stats.ReplayDetected()
|
|
||||||
return errors.New("replay attack is detected")
|
|
||||||
}
|
|
||||||
|
|
||||||
antireplay.Cache.AddTLS(clientHello.Random[:])
|
|
||||||
serverHello := tlstypes.NewServerHello(clientHello)
|
|
||||||
serverHelloPacket := serverHello.WelcomePacket()
|
|
||||||
|
|
||||||
if _, err := conn.Write(serverHelloPacket); err != nil {
|
|
||||||
return fmt.Errorf("cannot send welcome packet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientProtocol) cloakHost(clientConn io.ReadWriteCloser) {
|
|
||||||
stats.Stats.CloakedRequest()
|
|
||||||
|
|
||||||
addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort))
|
|
||||||
hostConn, err := net.Dial("tcp", addr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cloak(clientConn, hostConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeClientProtocol() protocol.ClientProtocol {
|
|
||||||
return &ClientProtocol{}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/wrappers/rwc"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cloakLastActivityTimeout = 5 * time.Second
|
|
||||||
cloakMaxTimeout = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func cloak(one, another io.ReadWriteCloser) {
|
|
||||||
defer func() {
|
|
||||||
one.Close()
|
|
||||||
another.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
channelPing := make(chan struct{}, 1)
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
one = rwc.NewPing(ctx, one, channelPing)
|
|
||||||
another = rwc.NewPing(ctx, another, channelPing)
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
|
|
||||||
wg.Add(2)
|
|
||||||
|
|
||||||
go cloakPipe(one, another, wg)
|
|
||||||
|
|
||||||
go cloakPipe(another, one, wg)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
lastActivityTimer := time.NewTimer(cloakLastActivityTimeout)
|
|
||||||
defer lastActivityTimer.Stop()
|
|
||||||
|
|
||||||
maxTimer := time.NewTimer(cloakMaxTimeout)
|
|
||||||
defer maxTimer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-channelPing:
|
|
||||||
lastActivityTimer.Stop()
|
|
||||||
lastActivityTimer = time.NewTimer(cloakLastActivityTimeout)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-lastActivityTimer.C:
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
case <-maxTimer.C:
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloakPipe(one io.Writer, another io.Reader, wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
buf := acquireCloakBuffer()
|
|
||||||
defer releaseCloakBuffer(buf)
|
|
||||||
|
|
||||||
io.CopyBuffer(one, another, *buf) // nolint: errcheck
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TimeSkew = 5 * time.Second
|
|
||||||
TimeFromBoot = 24 * 60 * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errBadDigest = errors.New("bad digest")
|
|
||||||
errBadTime = errors.New("bad time")
|
|
||||||
|
|
||||||
faketlsStartBytes = [...]byte{
|
|
||||||
0x16,
|
|
||||||
0x03,
|
|
||||||
0x01,
|
|
||||||
0x02,
|
|
||||||
0x00,
|
|
||||||
0x01,
|
|
||||||
0x00,
|
|
||||||
0x01,
|
|
||||||
0xfc,
|
|
||||||
0x03,
|
|
||||||
0x03,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package faketls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const cloakBufferSize = 1024
|
|
||||||
|
|
||||||
var (
|
|
||||||
poolBytesBuffer = sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return &bytes.Buffer{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
poolCloakBuffer = sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
rv := make([]byte, cloakBufferSize)
|
|
||||||
return &rv
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func acquireBytesBuffer() *bytes.Buffer {
|
|
||||||
return poolBytesBuffer.Get().(*bytes.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func acquireCloakBuffer() *[]byte {
|
|
||||||
return poolCloakBuffer.Get().(*[]byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func releaseBytesBuffer(buf *bytes.Buffer) {
|
|
||||||
buf.Reset()
|
|
||||||
poolBytesBuffer.Put(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func releaseCloakBuffer(buf *[]byte) {
|
|
||||||
poolCloakBuffer.Put(buf)
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,57 @@
|
|||||||
module github.com/9seconds/mtg
|
module github.com/9seconds/mtg/v2
|
||||||
|
|
||||||
go 1.13
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7
|
github.com/OneOfOne/xxhash v1.2.8
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d
|
github.com/alecthomas/kong v1.15.0
|
||||||
github.com/beevik/ntp v0.3.0
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
|
||||||
github.com/golang/protobuf v1.3.5 // indirect
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/prometheus/client_golang v1.5.1
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
|
||||||
github.com/prometheus/procfs v0.0.11 // indirect
|
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||||
github.com/smira/go-statsd v1.3.1
|
github.com/jarcoal/httpmock v1.0.8
|
||||||
go.uber.org/zap v1.14.1
|
github.com/mccutchen/go-httpbin v1.1.1
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
|
github.com/panjf2000/ants/v2 v2.12.0
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
github.com/prometheus/client_golang v1.23.2
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458 // indirect
|
github.com/rs/zerolog v1.35.0
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
github.com/smira/go-statsd v1.3.4
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
|
||||||
|
golang.org/x/crypto v0.49.0
|
||||||
|
golang.org/x/net v0.52.0
|
||||||
|
golang.org/x/sys v0.42.0
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beevik/ntp v1.5.0
|
||||||
|
github.com/ncruces/go-dns v1.3.3
|
||||||
|
github.com/pelletier/go-toml/v2 v2.3.0
|
||||||
|
github.com/pires/go-proxyproto v0.11.0
|
||||||
|
github.com/things-go/go-socks5 v0.1.0
|
||||||
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
|
||||||
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,192 +1,155 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
|
||||||
github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
|
|
||||||
github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||||
|
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
|
||||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/ncruces/go-dns v1.3.3 h1:59OV7XoJrTCoUMZjWRVs4GOjtntMTZqiQ5Mn+BT13hk=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/ncruces/go-dns v1.3.3/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
|
github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c=
|
||||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
|
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
|
||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
|
||||||
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
|
|
||||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/smira/go-statsd v1.3.1 h1:JalGiHNdK7GqVAPpg7j0Kwp2jZrz/fCg/B4ZuNuBY2w=
|
|
||||||
github.com/smira/go-statsd v1.3.1/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
|
||||||
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
|
github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA=
|
||||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
|
||||||
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e h1:xA7GVlbz6teIF4FdvuqwbX6C4tiqNk2PH7FRPIDerao=
|
||||||
|
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
|
||||||
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b h1:p+bJ3v5uUdEVMCoeFUs+BNJPsqt+Y6BLbDaPfTcbcH8=
|
||||||
|
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
||||||
|
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||||
|
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458 h1:DgonIcqC7u+gVZX7lpuReBil5B/i8fvW/hAQdhT6/ao=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.0.0-20200319210407-521f4a0cd458/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/mtproto"
|
|
||||||
"github.com/9seconds/mtg/mtproto/rpc"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const connectionTTL = time.Hour
|
|
||||||
|
|
||||||
type connection struct {
|
|
||||||
conn conntypes.PacketReadWriteCloser
|
|
||||||
proxyConns map[string]*ProxyConn
|
|
||||||
closeOnce sync.Once
|
|
||||||
proxyConnsMutex sync.RWMutex
|
|
||||||
id int
|
|
||||||
logger *zap.SugaredLogger
|
|
||||||
|
|
||||||
channelDone chan struct{}
|
|
||||||
channelWrite chan conntypes.Packet
|
|
||||||
channelRead chan *rpc.ProxyResponse
|
|
||||||
channelConnAttach chan *ProxyConn
|
|
||||||
channelConnDetach chan conntypes.ConnID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) run() {
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
ttl := time.NewTimer(connectionTTL)
|
|
||||||
defer ttl.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
for _, v := range c.proxyConns {
|
|
||||||
v.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
case <-ttl.C:
|
|
||||||
c.logger.Debugw("Closing connection by TTL")
|
|
||||||
c.Close()
|
|
||||||
case resp := <-c.channelRead:
|
|
||||||
if channel, ok := c.proxyConns[string(resp.ConnID[:])]; ok {
|
|
||||||
if resp.Type == rpc.ProxyResponseTypeCloseExt {
|
|
||||||
channel.Close()
|
|
||||||
} else {
|
|
||||||
channel.put(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case packet := <-c.channelWrite:
|
|
||||||
if err := c.conn.Write(packet); err != nil {
|
|
||||||
c.logger.Debugw("Cannot write packet", "error", err)
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
case conn := <-c.channelConnAttach:
|
|
||||||
c.proxyConnsMutex.Lock()
|
|
||||||
c.proxyConns[string(conn.req.ConnID[:])] = conn
|
|
||||||
c.proxyConnsMutex.Unlock()
|
|
||||||
conn.channelWrite = c.channelWrite
|
|
||||||
case connID := <-c.channelConnDetach:
|
|
||||||
if conn, ok := c.proxyConns[string(connID[:])]; ok {
|
|
||||||
c.proxyConnsMutex.Lock()
|
|
||||||
delete(c.proxyConns, string(connID[:]))
|
|
||||||
c.proxyConnsMutex.Unlock()
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) readLoop() {
|
|
||||||
for {
|
|
||||||
packet, err := c.conn.Read()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debugw("Cannot read packet", "error", err)
|
|
||||||
c.Close()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := rpc.ParseProxyResponse(packet)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debugw("Failed response", "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return
|
|
||||||
case c.channelRead <- response:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Close() {
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
c.logger.Debugw("Closing connection")
|
|
||||||
|
|
||||||
close(c.channelDone)
|
|
||||||
c.conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Done() bool {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return c.Len() == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Len() int {
|
|
||||||
c.proxyConnsMutex.RLock()
|
|
||||||
defer c.proxyConnsMutex.RUnlock()
|
|
||||||
|
|
||||||
return len(c.proxyConns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Attach(conn *ProxyConn) error {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
return ErrClosed
|
|
||||||
case c.channelConnAttach <- conn:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connection) Detach(connID conntypes.ConnID) {
|
|
||||||
select {
|
|
||||||
case <-c.channelDone:
|
|
||||||
case c.channelConnDetach <- connID:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConnection(req *protocol.TelegramRequest) (*connection, error) {
|
|
||||||
conn, err := mtproto.TelegramProtocol(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create a new connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := rand.Int() // nolint: gosec
|
|
||||||
rv := &connection{
|
|
||||||
conn: conn,
|
|
||||||
id: id,
|
|
||||||
logger: zap.S().Named("hub-connection").With("id", id,
|
|
||||||
"dc", req.ClientProtocol.DC(),
|
|
||||||
"protocol", req.ClientProtocol.ConnectionProtocol()),
|
|
||||||
proxyConns: make(map[string]*ProxyConn),
|
|
||||||
|
|
||||||
channelRead: make(chan *rpc.ProxyResponse, 1),
|
|
||||||
channelDone: make(chan struct{}),
|
|
||||||
channelWrite: make(chan conntypes.Packet),
|
|
||||||
channelConnAttach: make(chan *ProxyConn),
|
|
||||||
channelConnDetach: make(chan conntypes.ConnID),
|
|
||||||
}
|
|
||||||
|
|
||||||
go rv.readLoop()
|
|
||||||
|
|
||||||
go rv.run()
|
|
||||||
|
|
||||||
return rv, nil
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type connectionList struct {
|
|
||||||
connections []*connection
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) get(conn *ProxyConn) (*connection, error) {
|
|
||||||
if len(c.connections) > 0 && c.connections[0].Len() < config.C.MultiplexPerConnection {
|
|
||||||
if err := c.connections[0].Attach(conn); err == nil {
|
|
||||||
return c.connections[0], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newConn, err := newConnection(conn.req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot allocate a new connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = newConn.Attach(conn); err != nil {
|
|
||||||
newConn.Close()
|
|
||||||
return nil, fmt.Errorf("cannot attach to the newly created connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.connections = append(c.connections, newConn)
|
|
||||||
lastIndex := len(c.connections) - 1
|
|
||||||
c.connections[0], c.connections[lastIndex] = c.connections[lastIndex], c.connections[0]
|
|
||||||
|
|
||||||
return newConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) gc() {
|
|
||||||
prevLen := len(c.connections)
|
|
||||||
if prevLen == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(c.connections) - 1; i >= 0; i-- {
|
|
||||||
lastIndex := len(c.connections) - 1
|
|
||||||
|
|
||||||
if c.connections[i].Done() {
|
|
||||||
c.connections[i].Close()
|
|
||||||
|
|
||||||
if len(c.connections)-1 == i {
|
|
||||||
c.connections = c.connections[:lastIndex]
|
|
||||||
} else {
|
|
||||||
c.connections[i], c.connections[lastIndex] = c.connections[lastIndex], c.connections[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if prevLen != len(c.connections) {
|
|
||||||
c.sort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *connectionList) sort() {
|
|
||||||
if len(c.connections) > 1 {
|
|
||||||
sort.Slice(c.connections, func(i, j int) bool {
|
|
||||||
return c.connections[i].Len() < c.connections[j].Len()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hub struct {
|
|
||||||
muxes map[int32]*mux
|
|
||||||
mutex sync.RWMutex
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hub) Register(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
|
||||||
return h.getMux(req).Get(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hub) getMux(req *protocol.TelegramRequest) *mux {
|
|
||||||
var key int32 = 32767 + int32(req.ClientProtocol.DC()) + 100000*int32(req.ClientProtocol.ConnectionProtocol())
|
|
||||||
|
|
||||||
h.mutex.RLock()
|
|
||||||
m, ok := h.muxes[key]
|
|
||||||
h.mutex.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
h.mutex.Lock()
|
|
||||||
m, ok = h.muxes[key]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
m = newMux(h.ctx)
|
|
||||||
h.muxes[key] = m
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTimeout = errors.New("timeout")
|
|
||||||
ErrClosed = errors.New("context is closed")
|
|
||||||
|
|
||||||
Hub Interface
|
|
||||||
initOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(ctx context.Context) {
|
|
||||||
initOnce.Do(func() {
|
|
||||||
Hub = &hub{
|
|
||||||
muxes: make(map[int32]*mux),
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import "github.com/9seconds/mtg/protocol"
|
|
||||||
|
|
||||||
type Interface interface {
|
|
||||||
Register(*protocol.TelegramRequest) (*ProxyConn, error)
|
|
||||||
}
|
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const muxGCEvery = time.Minute
|
|
||||||
|
|
||||||
type muxNewRequest struct {
|
|
||||||
req *protocol.TelegramRequest
|
|
||||||
resp chan<- muxNewResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type muxNewResponse struct {
|
|
||||||
conn *ProxyConn
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type mux struct {
|
|
||||||
connections connectionList
|
|
||||||
clients map[string]*connection
|
|
||||||
ctx context.Context
|
|
||||||
channelClosed chan conntypes.ConnID
|
|
||||||
channelNew chan muxNewRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mux) run() {
|
|
||||||
gcTicker := time.NewTicker(muxGCEvery)
|
|
||||||
defer gcTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.ctx.Done():
|
|
||||||
for _, v := range m.clients {
|
|
||||||
v.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
case <-gcTicker.C:
|
|
||||||
m.connections.gc()
|
|
||||||
case req := <-m.channelNew:
|
|
||||||
m.connections.gc()
|
|
||||||
proxyConn := newProxyConn(req.req, m.channelClosed)
|
|
||||||
conn, err := m.connections.get(proxyConn)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
m.clients[string(req.req.ConnID[:])] = conn
|
|
||||||
}
|
|
||||||
|
|
||||||
req.resp <- muxNewResponse{
|
|
||||||
conn: proxyConn,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
close(req.resp)
|
|
||||||
case connID := <-m.channelClosed:
|
|
||||||
if conn, ok := m.clients[string(connID[:])]; ok {
|
|
||||||
conn.Detach(connID)
|
|
||||||
delete(m.clients, string(connID[:]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mux) Get(req *protocol.TelegramRequest) (*ProxyConn, error) {
|
|
||||||
resp := make(chan muxNewResponse)
|
|
||||||
m.channelNew <- muxNewRequest{
|
|
||||||
req: req,
|
|
||||||
resp: resp,
|
|
||||||
}
|
|
||||||
|
|
||||||
rv := <-resp
|
|
||||||
|
|
||||||
return rv.conn, rv.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMux(ctx context.Context) *mux {
|
|
||||||
m := &mux{
|
|
||||||
ctx: ctx,
|
|
||||||
clients: make(map[string]*connection),
|
|
||||||
channelClosed: make(chan conntypes.ConnID, 1),
|
|
||||||
channelNew: make(chan muxNewRequest),
|
|
||||||
}
|
|
||||||
go m.run()
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package hub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/conntypes"
|
|
||||||
"github.com/9seconds/mtg/mtproto/rpc"
|
|
||||||
"github.com/9seconds/mtg/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
proxyConnWriteTimeout = 2 * time.Minute
|
|
||||||
proxyConnReadTimeout = 2 * time.Minute
|
|
||||||
|
|
||||||
proxyConnBackpressureAfter = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProxyConn struct {
|
|
||||||
closeOnce sync.Once
|
|
||||||
req *protocol.TelegramRequest
|
|
||||||
channelResponse chan *rpc.ProxyResponse
|
|
||||||
channelClosed chan<- conntypes.ConnID
|
|
||||||
channelWrite chan<- conntypes.Packet
|
|
||||||
channelDone chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Read() (*rpc.ProxyResponse, error) {
|
|
||||||
timer := time.NewTimer(proxyConnReadTimeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
return nil, ErrTimeout
|
|
||||||
case <-p.channelDone:
|
|
||||||
return nil, ErrClosed
|
|
||||||
case packet := <-p.channelResponse:
|
|
||||||
return packet, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Write(packet conntypes.Packet) error {
|
|
||||||
timer := time.NewTimer(proxyConnWriteTimeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
return ErrTimeout
|
|
||||||
case <-p.channelDone:
|
|
||||||
return ErrClosed
|
|
||||||
case p.channelWrite <- packet:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) put(response *rpc.ProxyResponse) {
|
|
||||||
select {
|
|
||||||
case <-p.channelDone:
|
|
||||||
case p.channelResponse <- response:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyConn) Close() {
|
|
||||||
p.closeOnce.Do(func() {
|
|
||||||
close(p.channelDone)
|
|
||||||
go func() {
|
|
||||||
p.channelClosed <- p.req.ConnID
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProxyConn(req *protocol.TelegramRequest, channelClosed chan<- conntypes.ConnID) *ProxyConn {
|
|
||||||
return &ProxyConn{
|
|
||||||
channelResponse: make(chan *rpc.ProxyResponse, proxyConnBackpressureAfter),
|
|
||||||
channelDone: make(chan struct{}),
|
|
||||||
channelClosed: channelClosed,
|
|
||||||
req: req,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type accessResponse struct {
|
||||||
|
IPv4 *accessResponseURLs `json:"ipv4,omitempty"`
|
||||||
|
IPv6 *accessResponseURLs `json:"ipv6,omitempty"`
|
||||||
|
Secret struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
Base64 string `json:"base64"`
|
||||||
|
} `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type accessResponseURLs struct {
|
||||||
|
IP net.IP `json:"ip"`
|
||||||
|
Port uint `json:"port"`
|
||||||
|
TgURL string `json:"tg_url"` //nolint: tagliatelle
|
||||||
|
TgQrCode string `json:"tg_qrcode"` //nolint: tagliatelle
|
||||||
|
TmeURL string `json:"tme_url"` //nolint: tagliatelle
|
||||||
|
TmeQrCode string `json:"tme_qrcode"` //nolint: tagliatelle
|
||||||
|
}
|
||||||
|
|
||||||
|
type Access struct {
|
||||||
|
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
|
||||||
|
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` //nolint: lll
|
||||||
|
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` //nolint: lll
|
||||||
|
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` //nolint: lll
|
||||||
|
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Access) Run(cli *CLI, version string) error {
|
||||||
|
conf, err := utils.ReadConfig(a.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot init config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &accessResponse{}
|
||||||
|
resp.Secret.Base64 = conf.Secret.Base64()
|
||||||
|
resp.Secret.Hex = conf.Secret.Hex()
|
||||||
|
|
||||||
|
ntw, err := makeNetwork(conf, version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot init network: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
ip := a.PublicIPv4
|
||||||
|
if ip == nil {
|
||||||
|
ip = conf.PublicIPv4.Get(nil)
|
||||||
|
}
|
||||||
|
if ip == nil {
|
||||||
|
ip = getIP(ntw, "tcp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip != nil {
|
||||||
|
ip = ip.To4()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.IPv4 = a.makeURLs(conf, ip)
|
||||||
|
})
|
||||||
|
wg.Go(func() {
|
||||||
|
ip := a.PublicIPv6
|
||||||
|
if ip == nil {
|
||||||
|
ip = conf.PublicIPv6.Get(nil)
|
||||||
|
}
|
||||||
|
if ip == nil {
|
||||||
|
ip = getIP(ntw, "tcp6")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip != nil {
|
||||||
|
ip = ip.To16()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.IPv6 = a.makeURLs(conf, ip)
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := encoder.Encode(resp); err != nil {
|
||||||
|
return fmt.Errorf("cannot dump access json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs {
|
||||||
|
if ip == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
portNo := a.Port
|
||||||
|
if portNo == 0 {
|
||||||
|
portNo = conf.BindTo.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("server", ip.String())
|
||||||
|
values.Set("port", strconv.Itoa(int(portNo)))
|
||||||
|
|
||||||
|
if a.Hex {
|
||||||
|
values.Set("secret", conf.Secret.Hex())
|
||||||
|
} else {
|
||||||
|
values.Set("secret", conf.Secret.Base64())
|
||||||
|
}
|
||||||
|
|
||||||
|
urlQuery := values.Encode()
|
||||||
|
|
||||||
|
rv := &accessResponseURLs{
|
||||||
|
IP: ip,
|
||||||
|
Port: portNo,
|
||||||
|
TgURL: (&url.URL{
|
||||||
|
Scheme: "tg",
|
||||||
|
Host: "proxy",
|
||||||
|
RawQuery: urlQuery,
|
||||||
|
}).String(),
|
||||||
|
TmeURL: (&url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "t.me",
|
||||||
|
Path: "proxy",
|
||||||
|
RawQuery: urlQuery,
|
||||||
|
}).String(),
|
||||||
|
}
|
||||||
|
rv.TgQrCode = utils.MakeQRCodeURL(rv.TgURL)
|
||||||
|
rv.TmeQrCode = utils.MakeQRCodeURL(rv.TmeURL)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/alecthomas/kong"
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
|
||||||
|
Doctor Doctor `kong:"cmd,help='Check that proxy can run correctly'"`
|
||||||
|
Access Access `kong:"cmd,help='Print access information.'"`
|
||||||
|
Run Run `kong:"cmd,help='Run proxy.'"`
|
||||||
|
SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"`
|
||||||
|
Version kong.VersionFlag `kong:"help='Print version.',short='v'"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/utils"
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/beevik/ntp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tplError = template.Must(
|
||||||
|
template.New("").Parse(" ‼️ {{ .description }}: {{ .error }}\n"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tplWDeprecatedConfig = template.Must(
|
||||||
|
template.New("").
|
||||||
|
Parse(` ⚠️ Option {{ .old | printf "%q" }}{{ if .old_section }} from section [{{ .old_section }}]{{ end }} is deprecated and will be removed in v{{ .when }}. Please use {{ .new | printf "%q" }}{{ if .new_section }} in [{{ .new_section }}] section{{ end }} instead.` + "\n"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tplOTimeSkewness = template.Must(
|
||||||
|
template.New("").
|
||||||
|
Parse(" ✅ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}\n"),
|
||||||
|
)
|
||||||
|
tplWTimeSkewness = template.Must(
|
||||||
|
template.New("").
|
||||||
|
Parse(" ⚠️ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. Please check ntp.\n"),
|
||||||
|
)
|
||||||
|
tplETimeSkewness = template.Must(
|
||||||
|
template.New("").
|
||||||
|
Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tplODCConnect = template.Must(
|
||||||
|
template.New("").Parse(" ✅ DC {{ .dc }}\n"),
|
||||||
|
)
|
||||||
|
tplEDCConnect = template.Must(
|
||||||
|
template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"),
|
||||||
|
)
|
||||||
|
tplODCIPConnect = template.Must(
|
||||||
|
template.New("").Parse(" ✅ {{ .ip }}\n"),
|
||||||
|
)
|
||||||
|
tplEDCIPConnect = template.Must(
|
||||||
|
template.New("").Parse(" ❌ {{ .ip }}: {{ .error }}\n"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tplODNSSNIMatch = template.Must(
|
||||||
|
template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
|
||||||
|
)
|
||||||
|
tplEDNSSNIMatch = template.Must(
|
||||||
|
template.New("").Parse(" ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tplOFrontingDomain = template.Must(
|
||||||
|
template.New("").Parse(" ✅ {{ .address }} is reachable\n"),
|
||||||
|
)
|
||||||
|
tplEFrontingDomain = template.Must(
|
||||||
|
template.New("").Parse(" ❌ {{ .address }}: {{ .error }}\n"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Doctor struct {
|
||||||
|
conf *config.Config
|
||||||
|
|
||||||
|
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) Run(cli *CLI, version string) error {
|
||||||
|
conf, err := utils.ReadConfig(d.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot init config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.conf = conf
|
||||||
|
|
||||||
|
fmt.Println("Deprecated options")
|
||||||
|
everythingOK := d.checkDeprecatedConfig()
|
||||||
|
|
||||||
|
fmt.Println("Time skewness")
|
||||||
|
everythingOK = d.checkTimeSkewness() && everythingOK
|
||||||
|
|
||||||
|
resolver, err := network.GetDNS(conf.GetDNS())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create DNS resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := network.New(
|
||||||
|
resolver,
|
||||||
|
"",
|
||||||
|
conf.Network.Timeout.TCP.Get(10*time.Second),
|
||||||
|
conf.Network.Timeout.HTTP.Get(0),
|
||||||
|
conf.Network.Timeout.Idle.Get(0),
|
||||||
|
net.KeepAliveConfig{
|
||||||
|
Enable: !conf.Network.KeepAlive.Disabled.Get(false),
|
||||||
|
Idle: conf.Network.KeepAlive.Idle.Get(0),
|
||||||
|
Interval: conf.Network.KeepAlive.Interval.Get(0),
|
||||||
|
Count: int(conf.Network.KeepAlive.Count.Get(0)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println("Validate native network connectivity")
|
||||||
|
everythingOK = d.checkNetwork(base) && everythingOK
|
||||||
|
|
||||||
|
for _, url := range conf.Network.Proxies {
|
||||||
|
value, err := network.NewProxyNetwork(base, url.Get(nil))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Validate network connectivity with proxy %s\n", url.Get(nil))
|
||||||
|
everythingOK = d.checkNetwork(value) && everythingOK
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Validate fronting domain connectivity")
|
||||||
|
everythingOK = d.checkFrontingDomain(base) && everythingOK
|
||||||
|
|
||||||
|
fmt.Println("Validate SNI-DNS match")
|
||||||
|
everythingOK = d.checkSecretHost(resolver, base) && everythingOK
|
||||||
|
|
||||||
|
if !everythingOK {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkDeprecatedConfig() bool {
|
||||||
|
ok := true
|
||||||
|
|
||||||
|
if d.conf.DomainFrontingIP.Value != nil {
|
||||||
|
ok = false
|
||||||
|
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
|
||||||
|
"when": "2.3.0",
|
||||||
|
"old": "domain-fronting-ip",
|
||||||
|
"old_section": "",
|
||||||
|
"new": "ip",
|
||||||
|
"new_section": "domain-fronting",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.conf.DomainFrontingPort.Value != 0 {
|
||||||
|
ok = false
|
||||||
|
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
|
||||||
|
"when": "2.3.0",
|
||||||
|
"old": "domain-fronting-port",
|
||||||
|
"old_section": "",
|
||||||
|
"new": "port",
|
||||||
|
"new_section": "domain-fronting",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.conf.DomainFrontingProxyProtocol.Value {
|
||||||
|
ok = false
|
||||||
|
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
|
||||||
|
"when": "2.3.0",
|
||||||
|
"old": "domain-fronting-proxy-protocol",
|
||||||
|
"old_section": "",
|
||||||
|
"new": "proxy-protocol",
|
||||||
|
"new_section": "domain-fronting",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.conf.Network.DOHIP.Value != nil {
|
||||||
|
ok = false
|
||||||
|
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
|
||||||
|
"when": "2.3.0",
|
||||||
|
"old": "doh-ip",
|
||||||
|
"old_section": "network",
|
||||||
|
"new": "dns",
|
||||||
|
"new_section": "network",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
fmt.Println(" ✅ All good")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkTimeSkewness() bool {
|
||||||
|
response, err := ntp.Query("0.pool.ntp.org")
|
||||||
|
if err != nil {
|
||||||
|
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"description": "cannot access ntp pool",
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
skewness := response.ClockOffset.Abs()
|
||||||
|
confValue := d.conf.TolerateTimeSkewness.Get(mtglib.DefaultTolerateTimeSkewness)
|
||||||
|
diff := float64(skewness) / float64(confValue)
|
||||||
|
tplData := map[string]any{
|
||||||
|
"drift": response.ClockOffset,
|
||||||
|
"value": confValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case diff < 0.3:
|
||||||
|
tplOTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
|
||||||
|
return true
|
||||||
|
case diff < 0.7:
|
||||||
|
tplWTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
|
||||||
|
default:
|
||||||
|
tplETimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
|
||||||
|
dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses))
|
||||||
|
slices.Sort(dcs)
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
|
||||||
|
for _, dc := range dcs {
|
||||||
|
err := d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc])
|
||||||
|
if err == nil {
|
||||||
|
tplODCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"dc": dc,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tplEDCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"dc": dc,
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error {
|
||||||
|
checkAddresses := []string{}
|
||||||
|
|
||||||
|
switch d.conf.PreferIP.Get("prefer-ip4") {
|
||||||
|
case "only-ipv4":
|
||||||
|
for _, addr := range addresses {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
|
||||||
|
checkAddresses = append(checkAddresses, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "only-ipv6":
|
||||||
|
for _, addr := range addresses {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
|
||||||
|
checkAddresses = append(checkAddresses, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
checkAddresses = addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(checkAddresses) == 0 {
|
||||||
|
return fmt.Errorf("no suitable addresses after IP version filtering")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, addr := range checkAddresses {
|
||||||
|
conn, err = ntw.DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
tplEDCIPConnect.Execute(os.Stdout, map[string]any{
|
||||||
|
"error": err,
|
||||||
|
"ip": addr,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tplODCIPConnect.Execute(os.Stdout, map[string]any{
|
||||||
|
"ip": addr,
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.Close() //nolint: errcheck
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
|
||||||
|
host := d.conf.Secret.Host
|
||||||
|
if ip := d.conf.GetDomainFrontingIP(nil); ip != "" {
|
||||||
|
host = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
port := d.conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort)
|
||||||
|
address := net.JoinHostPort(host, strconv.Itoa(int(port)))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dialer := ntw.NativeDialer()
|
||||||
|
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
tplEFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"address": address,
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close() //nolint: errcheck
|
||||||
|
|
||||||
|
tplOFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"address": address,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
|
||||||
|
addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
|
||||||
|
if err != nil {
|
||||||
|
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ourIP4 := d.conf.PublicIPv4.Get(nil)
|
||||||
|
if ourIP4 == nil {
|
||||||
|
ourIP4 = getIP(ntw, "tcp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
ourIP6 := d.conf.PublicIPv6.Get(nil)
|
||||||
|
if ourIP6 == nil {
|
||||||
|
ourIP6 = getIP(ntw, "tcp6")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ourIP4 == nil && ourIP6 == nil {
|
||||||
|
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"description": "cannot detect public IP address",
|
||||||
|
"error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
strAddresses := []string{}
|
||||||
|
for _, value := range addresses {
|
||||||
|
if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
|
||||||
|
(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
|
||||||
|
tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"ip": value.IP,
|
||||||
|
"hostname": d.conf.Secret.Host,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
|
||||||
|
"hostname": d.conf.Secret.Host,
|
||||||
|
"resolved": strings.Join(strAddresses, ", "),
|
||||||
|
"ip4": ourIP4,
|
||||||
|
"ip6": ourIP6,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenerateSecret struct {
|
||||||
|
HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"`
|
||||||
|
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenerateSecret) Run(cli *CLI, _ string) error {
|
||||||
|
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
|
||||||
|
|
||||||
|
if g.Hex {
|
||||||
|
fmt.Println(secret.Hex()) //nolint: forbidigo
|
||||||
|
} else {
|
||||||
|
fmt.Println(secret.Base64()) //nolint: forbidigo
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Run struct {
|
||||||
|
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Run) Run(cli *CLI, version string) error {
|
||||||
|
conf, err := utils.ReadConfig(r.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot init config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runProxy(conf, version)
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/antireplay"
|
||||||
|
"github.com/9seconds/mtg/v2/events"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/proxyprotocol"
|
||||||
|
"github.com/9seconds/mtg/v2/internal/utils"
|
||||||
|
"github.com/9seconds/mtg/v2/ipblocklist"
|
||||||
|
"github.com/9seconds/mtg/v2/ipblocklist/files"
|
||||||
|
"github.com/9seconds/mtg/v2/logger"
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
"github.com/9seconds/mtg/v2/network/v2"
|
||||||
|
"github.com/9seconds/mtg/v2/stats"
|
||||||
|
"github.com/pires/go-proxyproto"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yl2chen/cidranger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeLogger(conf *config.Config) mtglib.Logger {
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||||
|
zerolog.TimestampFieldName = "timestamp"
|
||||||
|
zerolog.LevelFieldName = "level"
|
||||||
|
|
||||||
|
if conf.Debug.Get(false) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
} else {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||||
|
|
||||||
|
return logger.NewZeroLogger(baseLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
|
||||||
|
resolver, err := network.GetDNS(conf.GetDNS())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create DNS resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := network.New(
|
||||||
|
resolver,
|
||||||
|
"",
|
||||||
|
conf.Network.Timeout.TCP.Get(0),
|
||||||
|
conf.Network.Timeout.HTTP.Get(0),
|
||||||
|
conf.Network.Timeout.Idle.Get(0),
|
||||||
|
net.KeepAliveConfig{
|
||||||
|
Enable: !conf.Network.KeepAlive.Disabled.Get(false),
|
||||||
|
Idle: conf.Network.KeepAlive.Idle.Get(0),
|
||||||
|
Interval: conf.Network.KeepAlive.Interval.Get(0),
|
||||||
|
Count: int(conf.Network.KeepAlive.Count.Get(0)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
proxyDialers := make([]mtglib.Network, len(conf.Network.Proxies))
|
||||||
|
for idx, v := range conf.Network.Proxies {
|
||||||
|
value, err := network.NewProxyNetwork(base, v.Get(nil))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot use %v for proxy url: %w", v.Get(nil), err)
|
||||||
|
}
|
||||||
|
proxyDialers[idx] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(proxyDialers) {
|
||||||
|
case 0:
|
||||||
|
return base, nil
|
||||||
|
case 1:
|
||||||
|
return proxyDialers[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := network.Join(proxyDialers...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
|
||||||
|
if !conf.Defense.AntiReplay.Enabled.Get(false) {
|
||||||
|
return antireplay.NewNoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return antireplay.NewStableBloomFilter(
|
||||||
|
conf.Defense.AntiReplay.MaxSize.Get(antireplay.DefaultStableBloomFilterMaxSize),
|
||||||
|
conf.Defense.AntiReplay.ErrorRate.Get(antireplay.DefaultStableBloomFilterErrorRate),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeIPBlocklist(conf config.ListConfig,
|
||||||
|
logger mtglib.Logger,
|
||||||
|
ntw mtglib.Network,
|
||||||
|
updateCallback ipblocklist.FireholUpdateCallback,
|
||||||
|
) (mtglib.IPBlocklist, error) {
|
||||||
|
if !conf.Enabled.Get(false) {
|
||||||
|
return ipblocklist.NewNoop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteURLs := []string{}
|
||||||
|
localFiles := []string{}
|
||||||
|
|
||||||
|
for _, v := range conf.URLs {
|
||||||
|
if v.IsRemote() {
|
||||||
|
remoteURLs = append(remoteURLs, v.String())
|
||||||
|
} else {
|
||||||
|
localFiles = append(localFiles, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocklist, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
|
||||||
|
ntw,
|
||||||
|
conf.DownloadConcurrency.Get(1),
|
||||||
|
remoteURLs,
|
||||||
|
localFiles,
|
||||||
|
updateCallback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("incorrect parameters for firehol: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go blocklist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
|
||||||
|
|
||||||
|
return blocklist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeIPAllowlist(conf config.ListConfig,
|
||||||
|
logger mtglib.Logger,
|
||||||
|
ntw mtglib.Network,
|
||||||
|
updateCallback ipblocklist.FireholUpdateCallback,
|
||||||
|
) (mtglib.IPBlocklist, error) {
|
||||||
|
var (
|
||||||
|
allowlist mtglib.IPBlocklist
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if !conf.Enabled.Get(false) {
|
||||||
|
allowlist, err = ipblocklist.NewFireholFromFiles(
|
||||||
|
logger.Named("ipblocklist"),
|
||||||
|
1,
|
||||||
|
[]files.File{
|
||||||
|
files.NewMem([]*net.IPNet{
|
||||||
|
cidranger.AllIPv4,
|
||||||
|
cidranger.AllIPv6,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
updateCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
go allowlist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
|
||||||
|
} else {
|
||||||
|
allowlist, err = makeIPBlocklist(
|
||||||
|
conf,
|
||||||
|
logger,
|
||||||
|
ntw,
|
||||||
|
updateCallback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build allowlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) {
|
||||||
|
factories := make([]events.ObserverFactory, 0, 2)
|
||||||
|
|
||||||
|
if conf.Stats.StatsD.Enabled.Get(false) {
|
||||||
|
statsdFactory, err := stats.NewStatsd(
|
||||||
|
conf.Stats.StatsD.Address.Get(""),
|
||||||
|
logger.Named("statsd"),
|
||||||
|
conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
|
||||||
|
conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build statsd observer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
factories = append(factories, statsdFactory.Make)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Stats.Prometheus.Enabled.Get(false) {
|
||||||
|
prometheus := stats.NewPrometheus(
|
||||||
|
conf.Stats.Prometheus.MetricPrefix.Get(stats.DefaultMetricPrefix),
|
||||||
|
conf.Stats.Prometheus.HTTPPath.Get("/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", conf.Stats.Prometheus.BindTo.Get(""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot start a listener for prometheus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go prometheus.Serve(listener) //nolint: errcheck
|
||||||
|
|
||||||
|
factories = append(factories, prometheus.Make)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(factories) > 0 {
|
||||||
|
return events.NewEventStream(factories), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.NewNoopStream(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runProxy(conf *config.Config, version string) error { //nolint: funlen
|
||||||
|
logger := makeLogger(conf)
|
||||||
|
|
||||||
|
logger.BindJSON("configuration", conf.String()).Debug("configuration")
|
||||||
|
|
||||||
|
eventStream, err := makeEventStream(conf, logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build event stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ntw, err := makeNetwork(conf, version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build network: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocklist, err := makeIPBlocklist(
|
||||||
|
conf.Defense.Blocklist,
|
||||||
|
logger.Named("blocklist"),
|
||||||
|
ntw,
|
||||||
|
func(ctx context.Context, size int) {
|
||||||
|
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build ip blocklist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowlist, err := makeIPAllowlist(
|
||||||
|
conf.Defense.Allowlist,
|
||||||
|
logger.Named("allowlist"),
|
||||||
|
ntw,
|
||||||
|
func(ctx context.Context, size int) {
|
||||||
|
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build ip allowlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
|
||||||
|
for i, v := range conf.Defense.Doppelganger.URLs {
|
||||||
|
doppelGangerURLs[i] = v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := mtglib.ProxyOpts{
|
||||||
|
Logger: logger,
|
||||||
|
Network: ntw,
|
||||||
|
AntiReplayCache: makeAntiReplayCache(conf),
|
||||||
|
IPBlocklist: blocklist,
|
||||||
|
IPAllowlist: allowlist,
|
||||||
|
EventStream: eventStream,
|
||||||
|
|
||||||
|
Secret: conf.Secret,
|
||||||
|
Concurrency: conf.GetConcurrency(mtglib.DefaultConcurrency),
|
||||||
|
DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort),
|
||||||
|
DomainFrontingIP: conf.GetDomainFrontingIP(nil),
|
||||||
|
DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false),
|
||||||
|
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
|
||||||
|
AutoUpdate: conf.AutoUpdate.Get(false),
|
||||||
|
|
||||||
|
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
||||||
|
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
|
||||||
|
IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout),
|
||||||
|
HandshakeTimeout: conf.Network.Timeout.Handshake.Get(mtglib.DefaultHandshakeTimeout),
|
||||||
|
|
||||||
|
DoppelGangerURLs: doppelGangerURLs,
|
||||||
|
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
|
||||||
|
DoppelGangerEach: conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
|
||||||
|
DoppelGangerDRS: conf.Defense.Doppelganger.DRS.Get(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := mtglib.NewProxy(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create a proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := utils.NewListener(conf.BindTo.Get(""), 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot start proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.ProxyProtocolListener.Get(false) {
|
||||||
|
listener = &proxyprotocol.ListenerAdapter{
|
||||||
|
Listener: proxyproto.Listener{
|
||||||
|
Listener: listener,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := utils.RootContext()
|
||||||
|
|
||||||
|
go proxy.Serve(listener) //nolint: errcheck
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
listener.Close() //nolint: errcheck
|
||||||
|
proxy.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SimpleRun struct {
|
||||||
|
BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
|
||||||
|
Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
|
||||||
|
|
||||||
|
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` //nolint: lll
|
||||||
|
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` //nolint: lll
|
||||||
|
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll
|
||||||
|
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
|
||||||
|
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll
|
||||||
|
DomainFrontingIP string `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"` //nolint: lll
|
||||||
|
DOHIP net.IP `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"` //nolint: lll
|
||||||
|
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll
|
||||||
|
Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll
|
||||||
|
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` //nolint: lll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funlen
|
||||||
|
conf := &config.Config{}
|
||||||
|
|
||||||
|
if err := conf.BindTo.Set(s.BindTo); err != nil {
|
||||||
|
return fmt.Errorf("incorrect bind-to parameter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Secret.Set(s.Secret); err != nil {
|
||||||
|
return fmt.Errorf("incorrect secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil {
|
||||||
|
return fmt.Errorf("incorrect concurrency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.PreferIP.Set(s.PreferIP); err != nil {
|
||||||
|
return fmt.Errorf("incorrect prefer-ip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil {
|
||||||
|
return fmt.Errorf("incorrect domain-fronting-port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.DomainFrontingIP != "" {
|
||||||
|
if err := conf.DomainFrontingIP.Set(s.DomainFrontingIP); err != nil {
|
||||||
|
return fmt.Errorf("incorrect domain-fronting-ip: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil {
|
||||||
|
return fmt.Errorf("incorrect doh-ip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Network.Timeout.TCP.Set(s.Timeout.String()); err != nil {
|
||||||
|
return fmt.Errorf("incorrect timeout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Network.Timeout.HTTP.Set(s.Timeout.String()); err != nil {
|
||||||
|
return fmt.Errorf("incorrect timeout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Network.Timeout.Idle.Set(s.Timeout.String()); err != nil {
|
||||||
|
return fmt.Errorf("incorrect timeout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conf.Defense.AntiReplay.MaxSize.Set(s.AntiReplayCacheSize); err != nil {
|
||||||
|
return fmt.Errorf("incorrect antireplay-cache-size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Socks5Proxies {
|
||||||
|
proxyURL := config.TypeProxyURL{}
|
||||||
|
|
||||||
|
if err := proxyURL.Set(v); err != nil {
|
||||||
|
return fmt.Errorf("incorrect socks5 proxy URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Network.Proxies = append(conf.Network.Proxies, proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Debug.Value = s.Debug
|
||||||
|
conf.AllowFallbackOnUnknownDC.Value = true
|
||||||
|
conf.Defense.AntiReplay.Enabled.Value = true
|
||||||
|
|
||||||
|
if err := conf.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid result configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runProxy(conf, version)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/essentials"
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getIP(ntw mtglib.Network, protocol string) net.IP {
|
||||||
|
dialer := ntw.NativeDialer()
|
||||||
|
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
|
||||||
|
conn, err := dialer.DialContext(ctx, protocol, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return essentials.WrapNetConn(conn), err
|
||||||
|
})
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Accept", "text/plain")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
io.Copy(io.Discard, resp.Body) //nolint: errcheck
|
||||||
|
resp.Body.Close() //nolint: errcheck
|
||||||
|
}()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.ParseIP(strings.TrimSpace(string(data)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/mtglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Optional struct {
|
||||||
|
Enabled TypeBool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListConfig struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"`
|
||||||
|
URLs []TypeBlocklistURI `json:"urls"`
|
||||||
|
UpdateEach TypeDuration `json:"updateEach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Debug TypeBool `json:"debug"`
|
||||||
|
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
|
||||||
|
Secret mtglib.Secret `json:"secret"`
|
||||||
|
BindTo TypeHostPort `json:"bindTo"`
|
||||||
|
ProxyProtocolListener TypeBool `json:"proxyProtocolListener"`
|
||||||
|
PreferIP TypePreferIP `json:"preferIp"`
|
||||||
|
AutoUpdate TypeBool `json:"autoUpdate"`
|
||||||
|
DomainFrontingPort TypePort `json:"domainFrontingPort"`
|
||||||
|
DomainFrontingIP TypeIP `json:"domainFrontingIp"`
|
||||||
|
DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"`
|
||||||
|
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
|
||||||
|
Concurrency TypeConcurrency `json:"concurrency"`
|
||||||
|
PublicIPv4 TypeIP `json:"publicIpv4"`
|
||||||
|
PublicIPv6 TypeIP `json:"publicIpv6"`
|
||||||
|
DomainFronting struct {
|
||||||
|
IP TypeIP `json:"ip"`
|
||||||
|
Port TypePort `json:"port"`
|
||||||
|
ProxyProtocol TypeBool `json:"proxyProtocol"`
|
||||||
|
} `json:"domainFronting"`
|
||||||
|
Defense struct {
|
||||||
|
AntiReplay struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
MaxSize TypeBytes `json:"maxSize"`
|
||||||
|
ErrorRate TypeErrorRate `json:"errorRate"`
|
||||||
|
} `json:"antiReplay"`
|
||||||
|
Blocklist ListConfig `json:"blocklist"`
|
||||||
|
Allowlist ListConfig `json:"allowlist"`
|
||||||
|
Doppelganger struct {
|
||||||
|
URLs []TypeHttpsURL `json:"urls"`
|
||||||
|
Repeats TypeConcurrency `json:"repeats_per_raid"`
|
||||||
|
UpdateEach TypeDuration `json:"raid_each"`
|
||||||
|
DRS TypeBool `json:"drs"`
|
||||||
|
} `json:"doppelganger"`
|
||||||
|
} `json:"defense"`
|
||||||
|
Network struct {
|
||||||
|
Timeout struct {
|
||||||
|
TCP TypeDuration `json:"tcp"`
|
||||||
|
HTTP TypeDuration `json:"http"`
|
||||||
|
Idle TypeDuration `json:"idle"`
|
||||||
|
Handshake TypeDuration `json:"handshake"`
|
||||||
|
} `json:"timeout"`
|
||||||
|
KeepAlive struct {
|
||||||
|
Disabled TypeBool `json:"disabled"`
|
||||||
|
Idle TypeDuration `json:"idle"`
|
||||||
|
Interval TypeDuration `json:"interval"`
|
||||||
|
Count TypeConcurrency `json:"count"`
|
||||||
|
} `json:"keepAlive"`
|
||||||
|
DOHIP TypeIP `json:"dohIp"`
|
||||||
|
DNS TypeDNSURI `json:"dns"`
|
||||||
|
Proxies []TypeProxyURL `json:"proxies"`
|
||||||
|
} `json:"network"`
|
||||||
|
Stats struct {
|
||||||
|
StatsD struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
Address TypeHostPort `json:"address"`
|
||||||
|
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||||
|
TagFormat TypeStatsdTagFormat `json:"tagFormat"`
|
||||||
|
} `json:"statsd"`
|
||||||
|
Prometheus struct {
|
||||||
|
Optional
|
||||||
|
|
||||||
|
BindTo TypeHostPort `json:"bindTo"`
|
||||||
|
HTTPPath TypeHTTPPath `json:"httpPath"`
|
||||||
|
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||||
|
} `json:"prometheus"`
|
||||||
|
} `json:"stats"`
|
||||||
|
DCs struct{
|
||||||
|
DC1 []TypeHostPort `json:"dc1"`
|
||||||
|
DC2 []TypeHostPort `json:"dc2"`
|
||||||
|
DC3 []TypeHostPort `json:"dc3"`
|
||||||
|
DC4 []TypeHostPort `json:"dc4"`
|
||||||
|
DC5 []TypeHostPort `json:"dc5"`
|
||||||
|
DC203 []TypeHostPort `json:"dc203"`
|
||||||
|
DC10001 []TypeHostPort `json:"dc10001"`
|
||||||
|
DC10002[]TypeHostPort `json:"dc10002"`
|
||||||
|
DC10003 []TypeHostPort `json:"dc10003"`
|
||||||
|
} `json:"dc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetConcurrency(defaultValue uint) uint {
|
||||||
|
if concurrency := c.Concurrency.Get(0); concurrency != 0 {
|
||||||
|
return concurrency
|
||||||
|
}
|
||||||
|
return c.Concurrency.Get(defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDNS() *url.URL {
|
||||||
|
var dohURL *url.URL
|
||||||
|
|
||||||
|
if dohIP := c.Network.DOHIP.Get(nil); dohIP != nil {
|
||||||
|
dohURL, _ = url.Parse("https://" + dohIP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Network.DNS.Get(dohURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDomainFrontingPort(defaultValue uint) uint {
|
||||||
|
if port := c.DomainFronting.Port.Get(0); port != 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
return c.DomainFrontingPort.Get(defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDomainFrontingIP(defaultValue net.IP) string {
|
||||||
|
if ip := c.DomainFronting.IP.Get(nil); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
if ip := c.DomainFrontingIP.Get(defaultValue); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDomainFrontingProxyProtocol(defaultValue bool) bool {
|
||||||
|
return c.DomainFronting.ProxyProtocol.Get(false) || c.DomainFrontingProxyProtocol.Get(defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if !c.Secret.Valid() {
|
||||||
|
return fmt.Errorf("invalid secret %s", c.Secret.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BindTo.Get("") == "" {
|
||||||
|
return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) String() string {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
encoder := json.NewEncoder(buf)
|
||||||
|
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
|
||||||
|
if err := encoder.Encode(c); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) ReadConfig(filename string) []byte {
|
||||||
|
data, err := os.ReadFile(filepath.Join("testdata", filename))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParseEmpty() {
|
||||||
|
_, err := config.Parse([]byte{})
|
||||||
|
suite.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParseBrokenToml() {
|
||||||
|
_, err := config.Parse(suite.ReadConfig("broken.toml"))
|
||||||
|
suite.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParseOnlySecret() {
|
||||||
|
_, err := config.Parse(suite.ReadConfig("only_secret.toml"))
|
||||||
|
suite.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParseMinimalConfig() {
|
||||||
|
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64())
|
||||||
|
suite.Equal("0.0.0.0:3128", conf.BindTo.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParsePublicIP() {
|
||||||
|
conf, err := config.Parse(suite.ReadConfig("public_ip.toml"))
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
|
||||||
|
suite.Equal("2001:db8::1", conf.PublicIPv6.Get(nil).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParsePublicIPv4Only() {
|
||||||
|
conf, err := config.Parse(suite.ReadConfig("public_ip_v4_only.toml"))
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
|
||||||
|
suite.Nil(conf.PublicIPv6.Get(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParsePublicIPInvalid() {
|
||||||
|
_, err := config.Parse(suite.ReadConfig("public_ip_invalid.toml"))
|
||||||
|
suite.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
|
||||||
|
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Nil(conf.PublicIPv4.Get(nil))
|
||||||
|
suite.Nil(conf.PublicIPv6.Get(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestString() {
|
||||||
|
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotEmpty(conf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &ConfigTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tomlConfig struct {
|
||||||
|
Debug bool `toml:"debug" json:"debug,omitempty"`
|
||||||
|
AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"`
|
||||||
|
Secret string `toml:"secret" json:"secret"`
|
||||||
|
BindTo string `toml:"bind-to" json:"bindTo"`
|
||||||
|
ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"`
|
||||||
|
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
|
||||||
|
AutoUpdate bool `toml:"auto-update" json:"autoUpdate,omitempty"`
|
||||||
|
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
|
||||||
|
DomainFrontingIP string `toml:"domain-fronting-ip" json:"domainFrontingIp,omitempty"`
|
||||||
|
DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"`
|
||||||
|
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
|
||||||
|
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
|
||||||
|
PublicIPv4 string `toml:"public-ipv4" json:"publicIpv4,omitempty"`
|
||||||
|
PublicIPv6 string `toml:"public-ipv6" json:"publicIpv6,omitempty"`
|
||||||
|
DomainFronting struct {
|
||||||
|
IP string `toml:"ip" json:"ip,omitempty"`
|
||||||
|
Port uint `toml:"port" json:"port,omitempty"`
|
||||||
|
ProxyProtocol bool `toml:"proxy-protocol" json:"proxyProtocol,omitempty"`
|
||||||
|
} `toml:"domain-fronting" json:"domainFronting,omitempty"`
|
||||||
|
Defense struct {
|
||||||
|
AntiReplay struct {
|
||||||
|
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||||
|
MaxSize string `toml:"max-size" json:"maxSize,omitempty"`
|
||||||
|
ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"`
|
||||||
|
} `toml:"anti-replay" json:"antiReplay,omitempty"`
|
||||||
|
Blocklist struct {
|
||||||
|
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||||
|
DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"`
|
||||||
|
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||||
|
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
|
||||||
|
} `toml:"blocklist" json:"blocklist,omitempty"`
|
||||||
|
Allowlist struct {
|
||||||
|
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||||
|
DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"`
|
||||||
|
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||||
|
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
|
||||||
|
} `toml:"allowlist" json:"allowlist,omitempty"`
|
||||||
|
Doppelganger struct {
|
||||||
|
URLs []string `toml:"urls" json:"urls,omitempty"`
|
||||||
|
Repeats uint `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
|
||||||
|
UpdateEach string `toml:"raid-each" json:"raid_each,omitempty"`
|
||||||
|
DRS bool `toml:"drs" json:"drs,omitempty"`
|
||||||
|
} `toml:"doppelganger" json:"doppelganger,omitempty"`
|
||||||
|
} `toml:"defense" json:"defense,omitempty"`
|
||||||
|
Network struct {
|
||||||
|
Timeout struct {
|
||||||
|
TCP string `toml:"tcp" json:"tcp,omitempty"`
|
||||||
|
HTTP string `toml:"http" json:"http,omitempty"`
|
||||||
|
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||||
|
Handshake string `toml:"handshake" json:"handshake,omitempty"`
|
||||||
|
} `toml:"timeout" json:"timeout,omitempty"`
|
||||||
|
KeepAlive struct {
|
||||||
|
Disabled bool `toml:"disabled" json:"disabled,omitempty"`
|
||||||
|
Idle string `toml:"idle" json:"idle,omitempty"`
|
||||||
|
Interval string `toml:"interval" json:"interval,omitempty"`
|
||||||
|
Count uint `toml:"count" json:"count,omitempty"`
|
||||||
|
} `toml:"keep-alive" json:"keepAlive,omitempty"`
|
||||||
|
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
|
||||||
|
DNS string `toml:"dns" json:"dns,omitempty"`
|
||||||
|
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
|
||||||
|
} `toml:"network" json:"network,omitempty"`
|
||||||
|
Stats struct {
|
||||||
|
StatsD struct {
|
||||||
|
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||||
|
Address string `toml:"address" json:"address,omitempty"`
|
||||||
|
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||||
|
TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"`
|
||||||
|
} `toml:"statsd" json:"statsd,omitempty"`
|
||||||
|
Prometheus struct {
|
||||||
|
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
|
||||||
|
BindTo string `toml:"bind-to" json:"bindTo,omitempty"`
|
||||||
|
HTTPPath string `toml:"http-path" json:"httpPath,omitempty"`
|
||||||
|
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||||
|
} `toml:"prometheus" json:"prometheus,omitempty"`
|
||||||
|
} `toml:"stats" json:"stats,omitempty"`
|
||||||
|
DCs struct{
|
||||||
|
DC1 []string `toml:"dc1" json:"dc1,omitempty"`
|
||||||
|
DC2 []string `toml:"dc2" json:"dc2,omitempty"`
|
||||||
|
DC3 []string `toml:"dc3" json:"dc3,omitempty"`
|
||||||
|
DC4 []string `toml:"dc4" json:"dc4,omitempty"`
|
||||||
|
DC5 []string `toml:"dc5" json:"dc5,omitempty"`
|
||||||
|
DC203 []string `toml:"dc203" json:"dc203,omitempty"`
|
||||||
|
DC10001 []string `toml:"dc10001" json:"dc10001,omitempty"`
|
||||||
|
DC10002 []string `toml:"dc10002" json:"dc10002,omitempty"`
|
||||||
|
DC10003 []string `toml:"dc10003" json:"dc10003,omitempty"`
|
||||||
|
} `toml:"dc" json:"dc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(rawData []byte) (*Config, error) {
|
||||||
|
tomlConf := &tomlConfig{}
|
||||||
|
jsonBuf := &bytes.Buffer{}
|
||||||
|
conf := &Config{}
|
||||||
|
|
||||||
|
jsonEncoder := json.NewEncoder(jsonBuf)
|
||||||
|
jsonEncoder.SetEscapeHTML(false)
|
||||||
|
jsonEncoder.SetIndent("", "")
|
||||||
|
|
||||||
|
if err := toml.Unmarshal(rawData, tomlConf); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse toml config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jsonEncoder.Encode(tomlConf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse a config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
s = sdfsdfds
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
|
public-ipv4 = "203.0.113.1"
|
||||||
|
public-ipv6 = "2001:db8::1"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
|
public-ipv4 = "not-an-ip"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
|
||||||
|
bind-to = "0.0.0.0:3128"
|
||||||
|
public-ipv4 = "203.0.113.1"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeBlocklistURI struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBlocklistURI) Set(value string) error {
|
||||||
|
if stat, err := os.Stat(value); err == nil || os.IsExist(err) {
|
||||||
|
switch {
|
||||||
|
case stat.IsDir():
|
||||||
|
return fmt.Errorf("value is correct filepath but directory")
|
||||||
|
case stat.Mode().Perm()&0o400 == 0:
|
||||||
|
return fmt.Errorf("value is correct filepath but not readable")
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err = filepath.Abs(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"value is correct filepath but cannot resolve absolute (%s): %w",
|
||||||
|
value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("incorrect url (%s): %w", value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Host == "" {
|
||||||
|
return fmt.Errorf("incorrect url %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = parsedURL.String()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBlocklistURI) Get(defaultValue string) string {
|
||||||
|
if t.Value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBlocklistURI) IsRemote() bool {
|
||||||
|
return !filepath.IsAbs(t.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBlocklistURI) UnmarshalText(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBlocklistURI) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBlocklistURI) String() string {
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeBlocklistURITestStruct struct {
|
||||||
|
Value config.TypeBlocklistURI `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeBlocklistURITestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
directory string
|
||||||
|
absDirectory string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBlocklistURITestSuite) SetupSuite() {
|
||||||
|
dir, _ := os.Getwd()
|
||||||
|
absDir, _ := filepath.Abs(dir)
|
||||||
|
|
||||||
|
suite.directory = dir
|
||||||
|
suite.absDirectory = absDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"gopher://lalala",
|
||||||
|
"https:///paths",
|
||||||
|
"h:/=",
|
||||||
|
filepath.Join(suite.directory, "___"),
|
||||||
|
filepath.Join(suite.absDirectory, "___"),
|
||||||
|
suite.directory,
|
||||||
|
suite.absDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() {
|
||||||
|
testData := []string{
|
||||||
|
"http://lalala",
|
||||||
|
"https://lalala",
|
||||||
|
"https://lalala/path",
|
||||||
|
filepath.Join(suite.directory, "config.go"),
|
||||||
|
filepath.Join(suite.absDirectory, "config.go"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
testStruct := &typeBlocklistURITestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
assert.EqualValues(t, value, testStruct.Value.Get(""))
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "http") {
|
||||||
|
assert.True(t, testStruct.Value.IsRemote())
|
||||||
|
} else {
|
||||||
|
assert.False(t, testStruct.Value.IsRemote())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
|
||||||
|
testStruct := &typeBlocklistURITestStruct{
|
||||||
|
Value: config.TypeBlocklistURI{
|
||||||
|
Value: "http://some.url/with/path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(testStruct)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBlocklistURITestSuite) TestGet() {
|
||||||
|
value := config.TypeBlocklistURI{}
|
||||||
|
suite.Equal("/path", value.Get("/path"))
|
||||||
|
|
||||||
|
suite.NoError(value.Set("http://lalala.ru"))
|
||||||
|
suite.Equal("http://lalala.ru", value.Get("/path"))
|
||||||
|
suite.Equal("http://lalala.ru", value.Get(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeBlocklistURI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeBlocklistURITestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeBool struct {
|
||||||
|
Value bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBool) Set(data string) error {
|
||||||
|
parsed, err := strconv.ParseBool(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("incorrect bool value: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = parsed
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBool) Get(defaultValue bool) bool {
|
||||||
|
return t.Value || defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBool) UnmarshalJSON(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBool) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBool) String() string {
|
||||||
|
return strconv.FormatBool(t.Value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeBoolTestStruct struct {
|
||||||
|
Value config.TypeBool `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeBoolTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBoolTestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []any{
|
||||||
|
"",
|
||||||
|
"np",
|
||||||
|
"нет",
|
||||||
|
int(10),
|
||||||
|
[]int{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]any{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBoolTestSuite) TestUnmarshalOk() {
|
||||||
|
testData := []bool{
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
data, err := json.Marshal(map[string]bool{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||||
|
testStruct := &typeBoolTestStruct{}
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
|
||||||
|
if value {
|
||||||
|
assert.True(t, testStruct.Value.Value)
|
||||||
|
} else {
|
||||||
|
assert.False(t, testStruct.Value.Value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBoolTestSuite) TestMarshalOk() {
|
||||||
|
for _, v := range []bool{true, false} {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
suite.T().Run(strconv.FormatBool(v), func(t *testing.T) {
|
||||||
|
testStruct := typeBoolTestStruct{
|
||||||
|
Value: config.TypeBool{
|
||||||
|
Value: value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedJSON, err := json.Marshal(testStruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedJSON, err := json.Marshal(map[string]bool{
|
||||||
|
"value": value,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBoolTestSuite) TestGet() {
|
||||||
|
value := config.TypeBool{}
|
||||||
|
suite.False(value.Get(false))
|
||||||
|
suite.True(value.Get(true))
|
||||||
|
|
||||||
|
value.Value = true
|
||||||
|
suite.True(value.Get(false))
|
||||||
|
suite.True(value.Get(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeBool(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeBoolTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/units"
|
||||||
|
)
|
||||||
|
|
||||||
|
var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB")
|
||||||
|
|
||||||
|
type TypeBytes struct {
|
||||||
|
Value units.Base2Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBytes) Set(value string) error {
|
||||||
|
normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value))
|
||||||
|
|
||||||
|
parsedValue, err := units.ParseBase2Bytes(normalizedValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("incorrect bytes value (%v): %w", value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedValue < 0 {
|
||||||
|
return fmt.Errorf("bytes should be positive (%s)", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = parsedValue
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBytes) Get(defaultValue uint) uint {
|
||||||
|
if t.Value == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(t.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeBytes) UnmarshalText(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBytes) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeBytes) String() string {
|
||||||
|
if t.Value == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(t.Value.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeBytesTestStruct struct {
|
||||||
|
Value config.TypeBytes `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeBytesTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"1m",
|
||||||
|
"1",
|
||||||
|
"-1kb",
|
||||||
|
"-1kib",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBytesTestSuite) TestUnmarshalOk() {
|
||||||
|
testData := map[string]uint{
|
||||||
|
"1b": 1,
|
||||||
|
"1kb": 1024,
|
||||||
|
"1kib": 1024,
|
||||||
|
"2mb": 2 * 1024 * 1024,
|
||||||
|
"2mib": 2 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testData {
|
||||||
|
value := v
|
||||||
|
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": k,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(k, func(t *testing.T) {
|
||||||
|
testStruct := &typeBytesTestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
assert.EqualValues(t, value, testStruct.Value.Get(0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBytesTestSuite) TestMarshalOk() {
|
||||||
|
value := typeBytesTestStruct{}
|
||||||
|
suite.NoError(value.Value.Set("1kib"))
|
||||||
|
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.JSONEq(`{"value": "1kib"}`, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeBytesTestSuite) TestGet() {
|
||||||
|
value := config.TypeBytes{}
|
||||||
|
suite.EqualValues(1000, value.Get(1000))
|
||||||
|
|
||||||
|
suite.NoError(value.Set("1mib"))
|
||||||
|
suite.EqualValues(1048576, value.Get(1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeBytes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeBytesTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeConcurrency struct {
|
||||||
|
Value uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeConcurrency) Set(value string) error {
|
||||||
|
concurrencyValue, err := strconv.ParseUint(value, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value is not uint (%s): %w", value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if concurrencyValue == 0 {
|
||||||
|
return fmt.Errorf("value should be >0 (%s)", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = uint(concurrencyValue)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeConcurrency) Get(defaultValue uint) uint {
|
||||||
|
if t.Value == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeConcurrency) UnmarshalJSON(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeConcurrency) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeConcurrency) String() string {
|
||||||
|
return strconv.FormatUint(uint64(t.Value), 10)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeConcurrencyTestStruct struct {
|
||||||
|
Value config.TypeConcurrency `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeConcurrencyTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"-1",
|
||||||
|
"0",
|
||||||
|
"0.0",
|
||||||
|
"1.0",
|
||||||
|
"1.1",
|
||||||
|
".",
|
||||||
|
"some_value",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() {
|
||||||
|
testStruct := &typeConcurrencyTestStruct{}
|
||||||
|
|
||||||
|
suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct))
|
||||||
|
suite.EqualValues(1, testStruct.Value.Get(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeConcurrencyTestSuite) TestMarshalOk() {
|
||||||
|
testStruct := &typeConcurrencyTestStruct{
|
||||||
|
Value: config.TypeConcurrency{
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(testStruct)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.JSONEq(`{"value": 2}`, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeConcurrencyTestSuite) TestGet() {
|
||||||
|
value := config.TypeConcurrency{}
|
||||||
|
suite.EqualValues(1, value.Get(1))
|
||||||
|
|
||||||
|
value.Value = 3
|
||||||
|
suite.EqualValues(3, value.Get(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeConcurrency(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeConcurrencyTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeDC struct {
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDC) Set(value string) error {
|
||||||
|
parsed, err := strconv.ParseInt(value, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse dc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed < 0 {
|
||||||
|
parsed = -parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = int(parsed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDC) UnmarshalJSON(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) String() string {
|
||||||
|
return strconv.Itoa(t.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDC) Get() int {
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeDCTestStruct struct {
|
||||||
|
Value config.TypeDC `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeDCTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"-1s",
|
||||||
|
"1202002020202",
|
||||||
|
"xxx",
|
||||||
|
"-11111111111111",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeDCTestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestUnmarshalOk() {
|
||||||
|
testData := map[int]int{
|
||||||
|
1: 1,
|
||||||
|
-1: 1,
|
||||||
|
203: 203,
|
||||||
|
}
|
||||||
|
|
||||||
|
for value, expected := range testData {
|
||||||
|
data, err := json.Marshal(map[string]int{
|
||||||
|
"value": value,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(strconv.Itoa(value), func(t *testing.T) {
|
||||||
|
testStruct := &typeDCTestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
assert.Equal(t, expected, testStruct.Value.Value)
|
||||||
|
assert.Equal(t, expected, testStruct.Value.Get())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDCTestSuite) TestMarshalOk() {
|
||||||
|
testData := map[string]int{
|
||||||
|
"1": 1,
|
||||||
|
"203": 203,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testData {
|
||||||
|
value := k
|
||||||
|
expected := v
|
||||||
|
|
||||||
|
suite.T().Run(value, func(t *testing.T) {
|
||||||
|
testStruct := &typeDCTestStruct{}
|
||||||
|
|
||||||
|
assert.NoError(t, testStruct.Value.Set(value))
|
||||||
|
|
||||||
|
data, err := json.Marshal(testStruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedJSON, err := json.Marshal(map[string]int{
|
||||||
|
"value": expected,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeDC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeDCTestSuite{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeDNSURI struct {
|
||||||
|
Value *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) Set(value string) error {
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value is not URI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Host == "" {
|
||||||
|
parsed.Host = parsed.Path
|
||||||
|
parsed.Path = ""
|
||||||
|
parsed.Scheme = "udp"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parsed.Scheme {
|
||||||
|
case "https", "tls":
|
||||||
|
case "udp":
|
||||||
|
if ip := net.ParseIP(parsed.Hostname()); ip == nil {
|
||||||
|
return fmt.Errorf("simple DNS must IP address: %s", parsed.Hostname())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported DNS type %s", parsed.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Scheme != "https" && parsed.Path != "" {
|
||||||
|
return fmt.Errorf("path is supported only for DoH: %s", parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.User != nil {
|
||||||
|
return fmt.Errorf("used info is not supported: %s", parsed.User.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Value = parsed
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) Get(defaultValue *url.URL) *url.URL {
|
||||||
|
if t.Value != nil {
|
||||||
|
return t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeDNSURI) UnmarshalText(data []byte) error {
|
||||||
|
return t.Set(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDNSURI) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TypeDNSURI) String() string {
|
||||||
|
if t.Value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Value.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type typeDNSURITestStruct struct {
|
||||||
|
Value config.TypeDNSURI `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeDNSURITestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestUnmarshalFail() {
|
||||||
|
testData := []string{
|
||||||
|
"xx",
|
||||||
|
"ppar",
|
||||||
|
"",
|
||||||
|
"dns://hahaha",
|
||||||
|
"udp://xcxxcv",
|
||||||
|
"udp://1.1.1.1/xcv",
|
||||||
|
"1.1.1.1/xxx",
|
||||||
|
"tls://dns/xx",
|
||||||
|
"tls://1.1.1.1/xx",
|
||||||
|
"https://user:password@1.1.1.1",
|
||||||
|
"tls://user:password@1.1.1.1",
|
||||||
|
"udp://user:password@1.1.1.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
assert.Error(t, json.Unmarshal(data, &typeDNSURITestStruct{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestUnmarshalOk() {
|
||||||
|
testData := []string{
|
||||||
|
"1.1.1.1",
|
||||||
|
"tls://1.1.1.1",
|
||||||
|
"tls://dns.google",
|
||||||
|
"https://1.1.1.1",
|
||||||
|
"https://1.1.1.1/dns-query",
|
||||||
|
"https://dns.google",
|
||||||
|
"https://dns.google/dns-query",
|
||||||
|
"udp://1.1.1.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
data, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
testStruct := &typeDNSURITestStruct{}
|
||||||
|
assert.NoError(t, json.Unmarshal(data, testStruct))
|
||||||
|
if v == "1.1.1.1" {
|
||||||
|
v = "udp://" + v
|
||||||
|
}
|
||||||
|
assert.Equal(t, v, testStruct.Value.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestMarshalOk() {
|
||||||
|
testData := []string{
|
||||||
|
"tls://1.1.1.1",
|
||||||
|
"tls://dns.google",
|
||||||
|
"https://1.1.1.1",
|
||||||
|
"https://1.1.1.1/dns-query",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range testData {
|
||||||
|
suite.T().Run(v, func(t *testing.T) {
|
||||||
|
testStruct := &typePreferIPTestStruct{
|
||||||
|
Value: config.TypePreferIP{
|
||||||
|
Value: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedJSON, err := json.Marshal(testStruct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedJSON, err := json.Marshal(map[string]string{
|
||||||
|
"value": v,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypeDNSURITestSuite) TestGet() {
|
||||||
|
value := config.TypeDNSURI{}
|
||||||
|
suite.Nil(value.Get(nil))
|
||||||
|
|
||||||
|
suite.NoError(value.Set("tls://1.1.1.1"))
|
||||||
|
suite.NotNil(value.Get(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSURI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
suite.Run(t, &TypeDNSURITestSuite{})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user